openmausbot 0.1.96 → 0.1.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/assets/{DesktopViewer-DsnA23KM.js → DesktopViewer-CPidMCtt.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
  3. package/dist/assets/{arc-Duj5Coy2.js → arc-BUWGa9UU.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-uYUsDawP.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
  7. package/dist/assets/channel-DR8eD6QW.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-D-Uglxqq.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-CKV5D2F6.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
  34. package/dist/assets/{index-DreDnjOu.js → index-Cp4S4NOL.js} +1 -1
  35. package/dist/assets/index-CpaIXRLp.js +614 -0
  36. package/dist/assets/index-Z81SMsgR.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
  41. package/dist/assets/{linear-Bl669148.js → linear-BASsfWly.js} +1 -1
  42. package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-BVidiPkk.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +1 -1
  62. package/dist-server/drivers/agents-proxy.js +5 -5
  63. package/dist-server/index.js +2379 -1596
  64. package/dist-server/openmausbot.js +857 -1012
  65. package/dist-server/pair-cli.js +857 -1012
  66. package/dist-server/permission-proxy.js +0 -4
  67. package/dist-server/server/auto-approve.js +1 -1
  68. package/dist-server/server/auto-vm-claims.js +13 -1
  69. package/dist-server/server/automatic-recovery.js +1 -2
  70. package/dist-server/server/boat.js +68 -84
  71. package/dist-server/server/bot-overview.js +1 -1
  72. package/dist-server/server/browser-engine.js +38 -1
  73. package/dist-server/server/browser-live.js +112 -25
  74. package/dist-server/server/browser-runtime.js +31 -1
  75. package/dist-server/server/checkpoints.js +275 -44
  76. package/dist-server/server/cli-setup.js +3 -3
  77. package/dist-server/server/cloud-computer-tools.js +11 -4
  78. package/dist-server/server/cloud-home-start.js +1 -1
  79. package/dist-server/server/cloud-home.js +3 -9
  80. package/dist-server/server/cloud-owner.js +1 -1
  81. package/dist-server/server/computer-engine-removal.js +70 -0
  82. package/dist-server/server/computer-selection.js +18 -0
  83. package/dist-server/server/config.js +7 -11
  84. package/dist-server/server/connector-scope-hint.js +83 -0
  85. package/dist-server/server/default-model-selection.js +20 -14
  86. package/dist-server/server/delegations.js +11 -5
  87. package/dist-server/server/delta-context.js +6 -1
  88. package/dist-server/server/drivers/acp/core.js +1 -1
  89. package/dist-server/server/drivers/acp/opencode-go.js +16 -21
  90. package/dist-server/server/drivers/agents-call.js +1 -1
  91. package/dist-server/server/drivers/agents-catalog.js +3 -3
  92. package/dist-server/server/drivers/builtIn.js +0 -2
  93. package/dist-server/server/drivers/claude.js +49 -30
  94. package/dist-server/server/drivers/codex.js +2 -2
  95. package/dist-server/server/drivers/openai-chat.js +98 -6
  96. package/dist-server/server/drivers/pi.js +184 -49
  97. package/dist-server/server/index.js +555 -279
  98. package/dist-server/server/live-call.js +11 -6
  99. package/dist-server/server/managed-policy.js +1 -1
  100. package/dist-server/server/model-catalog/presets.js +8 -0
  101. package/dist-server/server/peer-roster.js +10 -2
  102. package/dist-server/server/room-turn-end.js +15 -0
  103. package/dist-server/server/routes/live.js +3 -3
  104. package/dist-server/server/routine-requests.js +1 -1
  105. package/dist-server/server/routines.js +1 -1
  106. package/dist-server/server/screen-frame-gate.js +1 -3
  107. package/dist-server/server/store.js +125 -1
  108. package/dist-server/server/surface.js +29 -49
  109. package/dist-server/server/system-prompt.js +10 -10
  110. package/dist-server/server/turn-continuation.js +162 -0
  111. package/dist-server/server/workspace.js +1 -1
  112. package/dist-server/shared/ask-question.js +0 -70
  113. package/dist-server/shared/cloud-computer.js +4 -6
  114. package/dist-server/shared/credential-request.js +1 -1
  115. package/dist-server/shared/failed-turn.js +1 -4
  116. package/dist-server/shared/place-view.js +409 -0
  117. package/dist-server/shared/tool-scope-support.js +1 -2
  118. package/package.json +1 -1
  119. package/dist/assets/channel-BZxwC-Gw.js +0 -1
  120. package/dist/assets/index-Du7o6Dsf.js +0 -610
  121. package/dist/assets/index-KxG4bWSr.css +0 -1
  122. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
  123. package/dist-server/server/drivers/boatagent.js +0 -443
@@ -53,6 +53,8 @@ import { avatarGenerationRequestSchema, avatarGenerationStateMatches, generateAv
53
53
  import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
54
54
  import { groupTurnCwd } from "./room-cwd.js";
55
55
  import { RoomTurnDeadline, RoomTurnStallRegistry, 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";
@@ -97,7 +99,7 @@ import { McpOAuthError, McpSignInError, McpOAuthManager, mcpOAuthRedirectUri, wi
97
99
  import { probeMcpServer } from "./mcp-probe.js";
98
100
  import { GROUP_GOAL_MAX_TURNS, groupGoalAssignmentKey, groupGoalCompletionTurnId, groupGoalCoordinatorInstructions, groupGoalWorkerInstructions, parseGroupGoalDecision, resolveGroupGoalMember, selectGroupGoalCoordinator, } from "./group-goal-run.js";
99
101
  import { BUILT_IN_DRIVERS } from "./drivers/builtIn.js";
100
- import { openCodeProviderKeysAllowed, setOpenCodeOwnProviderKeys, setOpenCodeProviderKeyPolicy } from "./drivers/acp/opencode-go.js";
102
+ import { openCodeProviderKeysAllowed, setOpenCodeProviderKeyPolicy } from "./drivers/acp/opencode-go.js";
101
103
  import { getOrCreateChannel, mirrorActivity, mirrorExchange, mirrorReply } from "./comms-visibility.js";
102
104
  import { readMessageText, recallMessages, recentMessages, searchMessagesAsync, closeMessageSearch, closeMessageDb, chatFollowups, cancelledChatFollowup, settleChatFollowups, threadsReferencing } from "./message-db.js";
103
105
  import { briefCrossingLabel, claimRecallCrossings, recallCrossingLabel } from "./recall-disclosure.js";
@@ -118,12 +120,13 @@ import { ProviderRegistry } from "./harness/registry.js";
118
120
  import { ManagedDesktopProviders } from "./managed-desktop.js";
119
121
  import { computerKindForResource, ManagedDesktopPolicy } from "./managed-policy.js";
120
122
  import { hostedModelPolicy, HOSTED_MODEL_POLICY_HEADER, HOSTED_PROVIDER_SETTINGS_ERROR } from "./hosted-models.js";
121
- import { boatNotConfiguredMessage, CLOUD_HOME_SECRET_KEYS, CLOUD_IGNORED_KEYS, CLOUD_PAIRING_PATH, cloudHomeConfiguration, cloudHomeOffersPlace, cloudHomePlaceRefusal, createCloudPairing, firstCloudTurnPatch, readSignedBody, } from "./cloud-home.js";
123
+ import { CLOUD_HOME_SECRET_KEYS, CLOUD_IGNORED_KEYS, CLOUD_PAIRING_PATH, cloudHomeConfiguration, cloudHomeOffersPlace, cloudHomePlaceRefusal, createCloudPairing, firstCloudTurnPatch, readSignedBody, } from "./cloud-home.js";
122
124
  import { CLOUD_PERSONAL_REFUSAL, settleCloudOwnership } from "./cloud-owner.js";
123
125
  import { createCloudMoveRoutes, workspaceShared } from "./cloud-move-http.js";
124
126
  import { RESTART_EXIT_CODE } from "./restart.js";
125
127
  import { holdIncludedServices } from "./included-services.js";
126
128
  import { selectDefaultModelSelection, withNewBotEffort } from "./default-model-selection.js";
129
+ import { computerEngineMoveText, removedComputerInstanceIds, writeComputerEngineMoveLines } from "./computer-engine-removal.js";
127
130
  import { cancelPeerApprovalsFor, cancelPeerApprovalsForThread, dismissStalePeerCards, peerApprovalFailure, requestPeerApproval, resolvePeerComms } from "./peer-approval.js";
128
131
  import { peerDeliveryReceipt } from "./peer-delivery.js";
129
132
  import { peerProvenanceNote, withPeerProvenance } from "./peer-provenance.js";
@@ -136,6 +139,7 @@ import { createLiveSession, liveAttachUrl, LiveSessionError } from "./live-call.
136
139
  import { LiveCallController, LiveCallSignedOutError } from "./live-call-controller.js";
137
140
  import { narrateTool, toUtterances } from "./tts/speech-text.js";
138
141
  import { turnStartLogLine } from "./turn-log.js";
142
+ import { makeCapContinuationSubscriber } from "./turn-continuation.js";
139
143
  import { buildRecoveryText, buildTurnContext, engineIsFresh, NATIVELY_REPLAYING_DRIVER_KINDS, peerMessageText } from "./turn-context.js";
140
144
  import { Handoffs, handedStateUsable, recordHanded, renderUnseen, sessionStart, unseenMessages, withUnseenMessages } from "./delta-context.js";
141
145
  import { extractTurnImages } from "./turn-images.js";
@@ -168,7 +172,7 @@ import { CalendarCallManager } from "./calendar-calls.js";
168
172
  import { BUILT_IN_BROWSER_SYSTEM_PROMPT } from "./browser-engine.js";
169
173
  import { BrowserRuntime, browserRuntimeEnv } from "./browser-runtime.js";
170
174
  import { BrowserLive } from "./browser-live.js";
171
- import { agentBrowserFrame, agentBrowserIntegration, browserEngineEncryptionKey, prepareBrowserSessionState, clearBrowserSessionState, closeBrowserSession, ensureChrome, installAgentBrowserBinary, resolveAgentBrowserBinary, browserEngineStatus, browserSessionId, describeBrowserEngine, } from "./browser-engine.js";
175
+ import { agentBrowserFrame, agentBrowserIntegration, browserEngineEncryptionKey, prepareBrowserSessionState, clearBrowserSessionState, closeBrowserSession, ensureChrome, installAgentBrowserBinary, resolveAgentBrowserBinary, browserEngineStatus, browserSessionId, describeBrowserEngine, setBrowserViewport, } from "./browser-engine.js";
172
176
  import { createScreenFrameSource } from "./screen-frame-source.js";
173
177
  import { screenFrameHash, screenSurfaceForTool, screenTouchingTool, settledFrameIsNews } from "./screen-frame-gate.js";
174
178
  import { asSchedule, RoutineRequestService } from "./routine-requests.js";
@@ -208,10 +212,11 @@ import { createTeamBackup, importTeamBackup } from "./team-backup.js";
208
212
  import { MAX_TEAM_BACKUP_BYTES } from "../shared/team-backup.js";
209
213
  import { shouldMountLocalComputer } from "./local-routing.js";
210
214
  import { autoLocalVmAttachable } from "./container-computer.js";
211
- import { startAutoVmClaim } from "./auto-vm-claims.js";
215
+ import { lazyClaimWaitMs, startAutoVmClaim } from "./auto-vm-claims.js";
212
216
  import { computerFreeAfterText, computerParkedText, computerStoppedWaitingText, computerWaitingText } from "./computer-wait.js";
213
217
  import { modelContextWindow } from "./model-context-window.js";
214
- import { cloudPlaceDriverError, parseSurface, PlaceUnavailableError, placeUnavailable, resolveSurface, surfaceLabel, surfaceOfComputerKind, surfacePrompt } from "./surface.js";
218
+ import { cloudPlaceRefusal, computerToolsRefusal, parseSurface, PlaceUnavailableError, placeUnavailable, resolveSurface, surfaceLabel, surfaceOfComputerKind, surfacePrompt } from "./surface.js";
219
+ import { cloudRefusal } from "../shared/place-view.js";
215
220
  import { PendingTurnCancellations, ProviderTurnGenerationRegistry, RetiredTurnRegistry, guardTurnDispatch, isTurnAdmissionBlocked, isTurnEventQuarantined, } from "./turn-dispatch-guard.js";
216
221
  import { createGracefulShutdown } from "./graceful-shutdown.js";
217
222
  import { acquireDataDirLeaseForProcess } from "./data-dir-lease.js";
@@ -244,6 +249,7 @@ import { localDesktopTarget, localVmViewerStatus, viewerTargetId } from "./deskt
244
249
  import { createAntigravityLeftoverRoutes } from "./routes/antigravity-leftovers.js";
245
250
  import { findAntigravityLeftovers, removeAntigravityLeftovers } from "./drivers/antigravity-temp.js";
246
251
  import { createLiveRoutes } from "./routes/live.js";
252
+ import { withScopeHint } from "./connector-scope-hint.js";
247
253
  const PORT = Number(process.env.OMB_PORT || process.env.OGB_PORT || 8799);
248
254
  const WEBHOOK_PORT = Number(process.env.OMB_WEBHOOK_PORT || PORT + 1);
249
255
  // Behind a proxy or tunnel, the base URL senders should use (docs/self-hosting.md).
@@ -558,7 +564,7 @@ function cloudLendingTurn(capability) {
558
564
  generation: capability.generation,
559
565
  thread: store.messagesFor(capability.threadId),
560
566
  starters: cloudThreadStarters(capability.threadId),
561
- reportsFromOthers: store.messagesFor(capability.threadId).some(cloudOthersRoutineReport),
567
+ reportsFromOthers: store.messagesFor(capability.threadId).some(cloudReportFromOthers),
562
568
  ownerPerson: cloudProvenOwnerPerson,
563
569
  cardAnswerer: cloudCardAnswerer,
564
570
  routineRun: () => {
@@ -601,11 +607,13 @@ function cloudThreadStarters(threadId) {
601
607
  function cloudLendingView(capability) {
602
608
  return CLOUD_HOME !== null && cloudHomeLendingRefusal(cloudLendingTurn(capability)) === null;
603
609
  }
604
- /** On a Cloud home: a routine's report (its lifecycle card, carrying what
605
- * the run said) of a routine the owner did not write, as it stands now. It
606
- * brings someone else's instructions' results into the conversation it
607
- * reports to. */
608
- function cloudOthersRoutineReport(line) {
610
+ /** On a Cloud home: a line that brings someone else's words into the
611
+ * conversation it lands in: a "post" webhook's payload, or a routine's
612
+ * report (its lifecycle card, carrying what the run said) of a routine the
613
+ * owner did not write, as it stands now. */
614
+ function cloudReportFromOthers(line) {
615
+ if (line.webhookPost)
616
+ return true;
609
617
  if (line.kind !== "routine.run" || !line.routineRun)
610
618
  return false;
611
619
  const routine = routines?.listRoutines().find((candidate) => candidate.id === line.routineRun.routineId);
@@ -627,7 +635,7 @@ function cloudOwnerOnlyThread(threadId) {
627
635
  const lastKey = last ? `${last.id}:${last.card?.answeredBy ? JSON.stringify(last.card.answeredBy) : ""}` : undefined;
628
636
  if (cached && cached.length === thread.length && cached.last === lastKey)
629
637
  return cached.owner;
630
- const owner = ownerOnlyConversation(thread, cloudProvenOwnerPerson, cloudCardAnswerer) && !thread.some(cloudOthersRoutineReport);
638
+ const owner = ownerOnlyConversation(thread, cloudProvenOwnerPerson, cloudCardAnswerer) && !thread.some(cloudReportFromOthers);
631
639
  if (ownerOnlyCache.size > 5_000)
632
640
  ownerOnlyCache.clear();
633
641
  ownerOnlyCache.set(threadId, { length: thread.length, last: lastKey, owner });
@@ -776,16 +784,11 @@ function approvalShape(shape) {
776
784
  /** Who opens a routine's results conversation on a Cloud home: the writer
777
785
  * of this request, else the owner for a routine that is theirs (they wrote
778
786
  * it as it stands, or they are its writer: cloud-owner.ts), else its last
779
- * writer, else nobody. A webhook's run is the owner's: only their own
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. */
787
+ * writer, else nobody. */
783
788
  function routineOpener(routineId) {
784
789
  if (routineWriterInFlight)
785
790
  return routineWriterInFlight;
786
791
  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
792
  if (routine && CLOUD_OWNER_KEY && cloudRoutineAuthors?.authored(routineId, routine))
790
793
  return CLOUD_OWNER_KEY;
791
794
  const writer = cloudRoutineAuthors?.writer(routineId);
@@ -868,7 +871,7 @@ function guestWorkThread(from, target, fromThreadId, brief) {
868
871
  * to have written, or a conversation or room a revoked session opened. What
869
872
  * the owner is told about one, and when its engine cannot be confined. */
870
873
  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 your Cloud was only yours, so this bot works in it without a shell. Start a new conversation to use it fully.";
874
+ 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
875
  function confinedWhy(threadId) {
873
876
  const run = activeRoutineRunForThread(threadId);
874
877
  const writer = run ? cloudRoutineAuthors?.writer(run.routineId) : undefined;
@@ -877,7 +880,7 @@ function confinedWhy(threadId) {
877
880
  function guestEngineRefusal(threadId) {
878
881
  const why = confinedWhy(threadId);
879
882
  return why === ROUTINE_FROM_BEFORE ? `This bot's engine can't run it that way. ${why}`
880
- : "This conversation is from before your Cloud was only yours, and this bot's engine can't work in it. Start a new conversation.";
883
+ : "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
884
  }
882
885
  /** On a Cloud home: a turn a guest drives, which runs in Ask whatever the
883
886
  * bot's own level (no Full access, no Auto reviewer, no saved commands), with
@@ -1528,8 +1531,6 @@ const openCodeKeysAllowed = () => openCodeProviderKeysAllowed({
1528
1531
  sharedSignIn: sharedSignIn(signInAllowList()),
1529
1532
  });
1530
1533
  setOpenCodeProviderKeyPolicy(openCodeKeysAllowed);
1531
- // Keys the owner saved for OpenCode in Settings reach it on every server.
1532
- setOpenCodeOwnProviderKeys(() => openCodeProviderKeys(cfg));
1533
1534
  let openCodeKeysLastAllowed = openCodeKeysAllowed();
1534
1535
  /** OpenCode lists a provider's models only while it may read that
1535
1536
  * provider's key. When enrollment or the sign-in list changes the answer,
@@ -1677,8 +1678,6 @@ function computerPlaceRefusal(kind) {
1677
1678
  const managed = managedPolicy.computerRefusal(kind);
1678
1679
  return managed ? { message: managed, code: "managed_policy" } : undefined;
1679
1680
  }
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
1681
  /** Why the organisation refuses this instance for bots, or undefined. */
1683
1682
  function policyModelRefusal(instance) {
1684
1683
  const engine = BUILT_IN_DRIVERS.find(driver => driver.driverKind === instance.driverKind)?.metadata.displayName;
@@ -1794,6 +1793,11 @@ function externalRuntimeMayCall(method, path) {
1794
1793
  return path === "/api/internal/agents" || /^\/api\/internal\/delegations\/[\w-]+$/.test(path);
1795
1794
  return method === "POST" && (path === "/api/internal/ask-bot" || path === "/api/internal/delegate-bot");
1796
1795
  }
1796
+ /** An external runtime polls check/wait_delegation for what it hands off, so
1797
+ * the result must not also wake the bot's own engine on the pinned thread. */
1798
+ function externalCompletion(capability) {
1799
+ return capability.generation === EXTERNAL_RUNTIME_GENERATION ? { completionOwner: "external" } : {};
1800
+ }
1797
1801
  function externalRuntimeCapability(header) {
1798
1802
  const grant = authorizeExternalRuntime(EXTERNAL_RUNTIMES_FILE, header, id => store.bot(id));
1799
1803
  if (!grant)
@@ -2087,14 +2091,63 @@ class ComputerWaitParked extends Error {
2087
2091
  this.generation = generation;
2088
2092
  }
2089
2093
  }
2090
- /** A place the turn was told to use failed to attach: its cause plus the one
2091
- * control that changes it (placeUnavailable). A cancelled setup, a parked
2092
- * computer wait and an already-worded place failure pass through as they
2093
- * are. The one wrapper for a bot thread and a room turn. */
2094
- function asPlaceFailure(error, place, source, limit) {
2094
+ /** A failed place never sticks: an Auto-recorded pin to it (a
2095
+ * select_computer switch, or where an earlier Auto turn landed) is the
2096
+ * machine's memory, not a choice, so the next message runs on Auto. A
2097
+ * person's pin stays. */
2098
+ function clearFailedAutoPin(botId, threadId, place) {
2099
+ const pinned = store.taskByThread(botId, threadId);
2100
+ if (pinned?.surface === place && pinned.surfaceSource === "auto") {
2101
+ store.patchTask(botId, threadId, { surface: undefined, surfaceSource: undefined });
2102
+ }
2103
+ }
2104
+ /** A place the turn was told to use failed to attach, read as one state of
2105
+ * shared/place-view.ts: a cloud computer's refusal by its code or words, any
2106
+ * other place by its own words. Its source decides the one way on. A
2107
+ * cancelled setup, a parked computer wait and an already-read place failure
2108
+ * pass through as they are. The one wrapper for a bot thread and a room
2109
+ * turn, at dispatch and at a cloud computer's start on its first call. */
2110
+ async function asPlaceFailure(error, place, source, bot, backend = "box") {
2095
2111
  if (error instanceof DirectTurnSetupCancelled || error instanceof ComputerWaitParked || error instanceof PlaceUnavailableError)
2096
2112
  return error;
2097
- return placeUnavailable(place, source, error instanceof Error ? error.message : String(error), limit);
2113
+ const failure = error instanceof Error ? error : new Error(String(error));
2114
+ const cause = failure.message.trim().replace(/[\s.]+$/, "");
2115
+ if (place !== "cloud" || backend === "vps") {
2116
+ return placeUnavailable(place, {
2117
+ state: "place-failed", params: { bot: bot.name, cause: `${cause.charAt(0).toUpperCase()}${cause.slice(1)}.` }, source,
2118
+ }, Boolean(CLOUD_HOME));
2119
+ }
2120
+ // A Boat refusal keeps the provider's own status and code (boat.ts
2121
+ // boatRefusal); a failed read of the account keeps its status.
2122
+ const { boatCode, boatStatus, status } = failure;
2123
+ const providerStatus = typeof boatStatus === "number" ? boatStatus : typeof status === "number" ? status : undefined;
2124
+ const read = cloudRefusal({
2125
+ message: failure.message,
2126
+ ...(typeof boatCode === "string" ? { code: boatCode } : {}),
2127
+ ...(providerStatus !== undefined ? { status: providerStatus } : {}),
2128
+ }, boat.boatAccount(cfg)?.included === true);
2129
+ const row = { state: read.state, params: { bot: bot.name, ...read.params }, source };
2130
+ if (row.state === "cc-at-once")
2131
+ row.params.holders = await cloudComputerHolders(bot.id);
2132
+ return placeUnavailable(place, row, Boolean(CLOUD_HOME));
2133
+ }
2134
+ /** The bots that hold this account's cloud computers, other than `botId`,
2135
+ * from this installation's own Boat list. Empty when it can't be read. */
2136
+ async function cloudComputerHolders(botId) {
2137
+ try {
2138
+ const listed = await boat.listManagedBoats(cfg, managedBoatOwners(), { adoptLegacy: false });
2139
+ return [...new Set(listed.instances
2140
+ .filter((instance) => instance.ownerBotId && instance.ownerBotId !== botId && instance.ownerName)
2141
+ .map((instance) => instance.ownerName))];
2142
+ }
2143
+ catch {
2144
+ return [];
2145
+ }
2146
+ }
2147
+ /** Cloud chosen with no cloud computers here: on a Cloud home they are the
2148
+ * plan's, so they are only unavailable; anywhere else they need a Boat key. */
2149
+ function boatNotConfigured(bot, source) {
2150
+ return placeUnavailable("cloud", { state: CLOUD_HOME ? "cc-unavailable" : "cc-needs-key", params: { bot: bot.name }, source }, Boolean(CLOUD_HOME));
2098
2151
  }
2099
2152
  const directTurnDispatchClaims = new Map();
2100
2153
  const directTurnGenerationByThread = new Map();
@@ -2520,6 +2573,7 @@ const browserRuntime = new BrowserRuntime({
2520
2573
  browserLive.closeForSession(session);
2521
2574
  return closed;
2522
2575
  },
2576
+ applyViewport: (spec) => setBrowserViewport(spec.command, spec.env),
2523
2577
  });
2524
2578
  const browserLive = new BrowserLive({ runtime: browserRuntime });
2525
2579
  // Temporary profiles last for this server run, but are never saved to disk.
@@ -2704,7 +2758,7 @@ function controlIntegration(botId, threadId, generation, machine = {}) {
2704
2758
  depth: 0,
2705
2759
  kind: "computer",
2706
2760
  ...(localVmTarget ? { localVmTarget } : {}),
2707
- ...(boxId ? { boxId } : {}),
2761
+ ...(boxId !== undefined ? { boxId } : {}),
2708
2762
  ...(teamComputerTurns.get(threadId) ? { teamComputerId: teamComputerTurns.get(threadId).computerId } : {}),
2709
2763
  skillAuthoring: false,
2710
2764
  createdBots: 0,
@@ -2767,17 +2821,89 @@ function askBotAndWait(targetBotId, message, depth, fromBotId, fromThreadId, tar
2767
2821
  }).catch((err) => finish({ status: "error", text: `(couldn't start that bot: ${err instanceof Error ? err.message : String(err)})` }));
2768
2822
  });
2769
2823
  }
2770
- // New bots honor setup's saved choice; unconfigured workspaces prefer Claude.
2771
- // While enrolled, a Company model that can run beats a signed-out personal
2772
- // engine, and the organisation's policy is respected; otherwise inert.
2773
- async function defaultSelection() {
2824
+ // New bots honor setup's saved choice; without one, an engine that can run a
2825
+ // turn now (Claude first). While enrolled, a Company model that can run beats
2826
+ // a signed-out personal engine, and the organisation's policy is respected;
2827
+ // otherwise inert. `null` leaves the saved choice out (moveOffComputerEngine).
2828
+ async function defaultSelection(saved = cfg.defaultModelSelection ?? null) {
2774
2829
  if (hostedModels)
2775
- return hostedModels.select(cfg.defaultModelSelection);
2776
- return selectDefaultModelSelection(await registry.describe(), cfg.defaultModelSelection, {
2830
+ return hostedModels.select(saved ?? undefined);
2831
+ return selectDefaultModelSelection(await registry.describe(), saved ?? undefined, {
2777
2832
  company: (instanceId) => managedDesktop.owns(instanceId),
2778
2833
  refusal: (instance) => policyModelRefusal(instance),
2779
2834
  });
2780
2835
  }
2836
+ /** The Computer engine was removed: it ran a whole turn on Boat's own agent.
2837
+ * Everything still set to it moves, once, to the engine a new bot gets
2838
+ * (defaultSelection, the one picker), or the one a new bot would get without
2839
+ * the saved choice when that cannot run: the saved default and new-bot
2840
+ * defaults, each bot and conversation (Store.retireInstances keeps each one
2841
+ * where it worked), and an automatic-recovery backup, which could never run
2842
+ * and is cleared. Each moved conversation says so in one plain line. When no
2843
+ * engine is available yet it runs again the next time the engines are read
2844
+ * (describeInstances): an install, a sign-in, a key or a Company engine. At
2845
+ * start it sees what a new bot made then would see; a policy that arrives
2846
+ * later refuses an engine for a moved bot the way it does for any bot. A
2847
+ * hosted workspace's reconcile has already moved every bot onto its own
2848
+ * models. */
2849
+ async function moveOffComputerEngine() {
2850
+ if (hostedModels)
2851
+ return;
2852
+ const removed = removedComputerInstanceIds(cfg.instances);
2853
+ const named = (selection) => Boolean(selection && removed.has(selection.instanceId));
2854
+ if (named(cfg.automaticRecovery?.backup)) {
2855
+ saveConfig({ automaticRecovery: { enabled: false } });
2856
+ cfg.automaticRecovery = { enabled: false };
2857
+ }
2858
+ const defaults = named(cfg.defaultModelSelection) || named(cfg.newBotDefaults?.profile.modelSelection);
2859
+ if (!defaults && !store.bots.some(bot => named(bot.modelSelection) || bot.fallback?.some(named) ||
2860
+ store.tasks(bot.id).some(task => named(task.modelSelection))))
2861
+ return;
2862
+ let replacement = named(cfg.defaultModelSelection) ? { instanceId: "", model: "" } : await defaultSelection();
2863
+ if (!replacement.instanceId)
2864
+ replacement = await defaultSelection(null);
2865
+ if (!replacement.instanceId) {
2866
+ console.warn("[engines] the Computer engine was removed and no other engine is available yet; its bots move when one is");
2867
+ return;
2868
+ }
2869
+ if (defaults) {
2870
+ // One write keeps both saved defaults on the replacement (saveConfig
2871
+ // syncs them), and the copies in memory follow at once, so a bot made
2872
+ // before the next start never lands on the removed engine.
2873
+ const newBotDefaults = cfg.newBotDefaults && newBotDefaultsSchema.parse({
2874
+ ...cfg.newBotDefaults, profile: { ...cfg.newBotDefaults.profile, modelSelection: replacement },
2875
+ });
2876
+ saveConfig(newBotDefaults ? { newBotDefaults } : { defaultModelSelection: replacement });
2877
+ cfg.defaultModelSelection = replacement;
2878
+ if (newBotDefaults)
2879
+ cfg.newBotDefaults = newBotDefaults;
2880
+ }
2881
+ const instance = registry.get(replacement.instanceId);
2882
+ const engine = instance?.displayName ?? instance?.driverKind ?? replacement.instanceId;
2883
+ const moves = store.retireInstances(removed, replacement, {
2884
+ driverKind: instance?.driverKind,
2885
+ canWorkOnCloud: canWorkOnCloud(cloudEngine(instance)),
2886
+ keepCloud: (bot) => boat.boatConfigured(cfg) && bot.cloudBackend !== "vps" && !inheritedTeamComputer(bot),
2887
+ });
2888
+ writeComputerEngineMoveLines(moves, (move) => {
2889
+ const bot = store.bot(move.botId);
2890
+ if (bot)
2891
+ store.appendMessage(move.threadId, { role: "bot", kind: "activity", tool: { name: computerEngineMoveText(move, bot.name, engine), ok: true } });
2892
+ });
2893
+ }
2894
+ let computerEngineMoveRunning = null;
2895
+ /** The move, once at a time; it returns at once when nothing names the
2896
+ * removed engine any more. A failure is logged and never rejects, so it never
2897
+ * stops this server from listening. Bots whose save failed are unchanged and
2898
+ * move the next time the engines are read (describeInstances) or a turn
2899
+ * reaches one of them (startTurn), so a server no app reads the engines from
2900
+ * still moves them. */
2901
+ function retryComputerEngineMove() {
2902
+ computerEngineMoveRunning ??= moveOffComputerEngine()
2903
+ .catch((error) => console.warn(`[engines] moving bots off the removed Computer engine failed: ${error instanceof Error ? error.message : String(error)}`))
2904
+ .finally(() => { computerEngineMoveRunning = null; });
2905
+ return computerEngineMoveRunning;
2906
+ }
2781
2907
  function checkedModelSelection(raw, current, requireAvailableModel = false) {
2782
2908
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2783
2909
  return { ok: false, status: 400, error: "modelSelection must be an object" };
@@ -3081,6 +3207,8 @@ const store = new Store(() => bootSelection, (selection) => withNewBotEffort(sel
3081
3207
  const teamComputers = new TeamComputers(join(DATA_DIR, "team-computers.json"), ENVIRONMENT_ID);
3082
3208
  let followupsReady = false;
3083
3209
  const sendSequencer = new SendSequencer();
3210
+ // Before the new-bot default is read: a saved default may name the engine.
3211
+ await retryComputerEngineMove();
3084
3212
  bootSelection = await defaultSelection();
3085
3213
  store.seedIfEmpty();
3086
3214
  hostedModels?.reconcile(store);
@@ -3190,7 +3318,6 @@ function previewSystemPrompt(bot) {
3190
3318
  const computerPromptKind = resolveComputerPromptKind({
3191
3319
  kind: previewComputer === "vm" ? "vm" : previewComputer === "cloud"
3192
3320
  ? bot.cloudBackend === "vps" ? "vps" : "box" : previewComputer === "local" ? "local" : null,
3193
- driverKind: instance?.driverKind,
3194
3321
  vmPrivate: localVmMode(cfg) === "per-bot",
3195
3322
  });
3196
3323
  const peers = reachablePeers(store.bots, bot);
@@ -3230,7 +3357,7 @@ function previewSystemPrompt(bot) {
3230
3357
  computer: previewPlan.computer && previewPlan.computer !== "off" && computerPromptKind ? previewPlan.computer : null,
3231
3358
  browser: previewPlan.computer === undefined ? false : previewPlan.browser,
3232
3359
  }, { note: previewPlan.note, cloudHome: Boolean(CLOUD_HOME) }) },
3233
- { id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(agentsMounted && lendingEnabled()) : "" },
3360
+ { id: "cloud-home", label: "My Cloud", text: CLOUD_HOME ? cloudHomePrompt(agentsMounted && lendingEnabled()) : "" },
3234
3361
  { id: "composio", label: "Connected apps", text: caps?.composioMcp && bot.composio !== false && composio.configured(cfg) ? composioSystemPrompt(bot.connectorTools) + describeConnectorScopes(bot.connectorScopes) : "" },
3235
3362
  { id: "mcp", label: "MCP servers", text: caps?.customMcp ? customMcpPrompt(Object.keys(engineMcpServers(bot))) : "" },
3236
3363
  { id: "browser", label: "Browser", text: previewPlan.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
@@ -5449,6 +5576,10 @@ bus.subscribe((event) => {
5449
5576
  else if (event.type !== "session.exited")
5450
5577
  watchdog.touch(event.threadId);
5451
5578
  });
5579
+ // Automatic continuity: a turn that died on its budget or on tool errors gets
5580
+ // a `Continue:` task on the same bot, seeded with the persisted handoff — the
5581
+ // work moves forward without a person noticing and re-dispatching.
5582
+ bus.subscribe(makeCapContinuationSubscriber({ store, startTurn }));
5452
5583
  // Memory journal turn boundary (server/memory-journal.ts). A bot's own
5453
5584
  // file-tool writes to MEMORY.md and memory/ have no hook to tap, so the
5454
5585
  // diff against the baseline taken at dispatch is made when the turn
@@ -5945,7 +6076,7 @@ function inheritedTeamComputer(bot) {
5945
6076
  return teamComputers.forBot(bot);
5946
6077
  }
5947
6078
  function teamComputerPrompt(computer) {
5948
- return computer ? `Your team shares the Boat 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.` : "";
6079
+ 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
6080
  }
5950
6081
  function botComputerControlKey(bot) {
5951
6082
  const computer = inheritedTeamComputer(bot);
@@ -5956,21 +6087,26 @@ async function computerCallGate(internalCapability) {
5956
6087
  const snapshot = botComputerControlSnapshot(botId, internalCapability.teamComputerId);
5957
6088
  const slot = autoVmClaims.get(internalCapability.threadId);
5958
6089
  const lazyClaim = slot && slot.owner.generation === internalCapability.generation ? slot : undefined;
5959
- if (!snapshot.held && lazyClaim?.lazy && !lazyClaim.begin) {
5960
- // First screen tools/call on a lazily-attached Auto VM (issue
6090
+ const waitMs = snapshot.held ? null : lazyClaimWaitMs(lazyClaim, LAZY_VM_CLAIM_GRACE_MS);
6091
+ if (lazyClaim && waitMs !== null) {
6092
+ // First screen tools/call on a lazily-attached computer (issue
5961
6093
  // #1361): fire the exclusive claim — once — and give it a moment
5962
6094
  // to land. A free, ready VM claims in the time of one container
5963
6095
  // inspect, so this call then proceeds with an honest answer;
5964
6096
  // only a claim still queued behind another holder answers held
5965
- // below, and then the contention text is true. Keyed on the
5966
- // slot, never on the thread's turn-computer entry: a bind this
6097
+ // below, and then the contention text is true. A cloud computer's
6098
+ // claim creates or wakes it, so every call waits on that start
6099
+ // (graceMs) instead of being told someone else holds it. Keyed on
6100
+ // the slot, never on the thread's turn-computer entry: a bind this
5967
6101
  // turn abandoned earlier (a VPS that turned out to be asleep)
5968
6102
  // must not hide the unclaimed VM and let the call through.
5969
6103
  startAutoVmClaim(autoVmClaims, internalCapability.threadId, internalCapability.generation);
6104
+ let timer;
5970
6105
  await Promise.race([
5971
6106
  lazyClaim.begin ?? Promise.resolve(),
5972
- new Promise((resolve) => setTimeout(resolve, LAZY_VM_CLAIM_GRACE_MS)),
6107
+ new Promise((resolve) => { timer = setTimeout(resolve, waitMs); }),
5973
6108
  ]);
6109
+ clearTimeout(timer);
5974
6110
  }
5975
6111
  if (!snapshot.held && lazyClaim?.failed === true) {
5976
6112
  // A rejected lazy claim (gate finding F1, issue #1361): the
@@ -5990,10 +6126,11 @@ async function computerCallGate(internalCapability) {
5990
6126
  }
5991
6127
  if (!snapshot.held && lazyClaim?.lazy && lazyClaim.begin && !lazyClaim.claimed) {
5992
6128
  // The claim fired and is still waiting on the exclusive bind:
5993
- // another turn genuinely holds this desktop right now.
6129
+ // another turn genuinely holds this desktop right now. A slow claim
6130
+ // says what it is still doing instead.
5994
6131
  return {
5995
6132
  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.",
6133
+ 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
6134
  };
5998
6135
  }
5999
6136
  const computer = turnComputerResources.get(internalCapability.threadId);
@@ -6019,11 +6156,6 @@ function teamComputerInUse(computer) {
6019
6156
  [...teamComputerTurns.values()].some(turn => turn.computerId === computer.id) ||
6020
6157
  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
6158
  }
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
6159
  function claimTeamComputerLifecycle(computer) {
6028
6160
  if (computerProviderConfigTransitions.has("box"))
6029
6161
  throw Object.assign(new Error(providerTransitionMessage("box")), { status: 409 });
@@ -6060,9 +6192,9 @@ async function teamComputersPayload() {
6060
6192
  }
6061
6193
  /** The same named Boat identity and whole-turn lease in chats and rooms.
6062
6194
  * Assignment authorizes waking, never replacing a missing paid machine. */
6063
- async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
6195
+ async function attachTeamBoat(computer, botId, owner, canMount) {
6064
6196
  if (!canMount)
6065
- throw new Error("This model cannot use the team's Boat computer; choose a model provider with computer tools or an explicit bot destination");
6197
+ 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
6198
  if (!boat.boatConfigured(cfg))
6067
6199
  throw new Error("The team's Boat account is not configured; reconnect it in Settings");
6068
6200
  const ownerId = teamComputerOwner(computer.id);
@@ -6071,46 +6203,51 @@ async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
6071
6203
  if (computerControl.snapshot(ownerId).held)
6072
6204
  throw new Error("Release human control of the team computer before starting another turn");
6073
6205
  await bindTurnComputer(owner, `computer:box-bot:${ownerId}`, true);
6074
- teamComputerTurns.set(owner.threadId, { owner, computerId: computer.id, botId, remoteAgent });
6206
+ teamComputerTurns.set(owner.threadId, { owner, computerId: computer.id, botId });
6075
6207
  let machine = await boat.findBoat(cfg, ownerId);
6076
6208
  if (!machine)
6077
- throw new Error("The team's Boat computer is missing; explicitly create or retry it from the Team map");
6209
+ throw new Error("The team's cloud computer is missing; explicitly create or retry it from the Team map");
6078
6210
  await bindTurnComputer(owner, `computer:box:${machine.id}`, true);
6079
- const action = boat.boatTurnLifecycleAction({ explicitCloud: true, state: typeof machine.state === "string" ? machine.state : null });
6211
+ const action = boat.boatTurnLifecycleAction(typeof machine.state === "string" ? machine.state : null);
6080
6212
  if (action === "wake")
6081
6213
  machine = await boat.readyBoat(cfg, ownerId);
6082
- if (!machine || boat.boatTurnLifecycleAction({ explicitCloud: true, state: typeof machine.state === "string" ? machine.state : null }) !== "attach") {
6214
+ if (!machine || boat.boatTurnLifecycleAction(typeof machine.state === "string" ? machine.state : null) !== "attach") {
6083
6215
  throw new Error("The team computer is not ready; check it in the Team map");
6084
6216
  }
6085
6217
  if (turnResourceOwners.get(owner.threadId)?.generation !== owner.generation ||
6086
6218
  !turnResources.owns(`computer:box:${machine.id}`, owner))
6087
6219
  throw new Error("This computer turn ended while its machine was starting");
6088
6220
  return {
6089
- mount: cloudComputerMount(botId, owner, machine.id, remoteAgent),
6221
+ mount: cloudComputerMount(botId, owner, machine.id),
6090
6222
  capture: () => boat.screenshotBoat(cfg, ownerId, machine.id),
6091
6223
  };
6092
6224
  }
6093
- /** The two engine facts the cloud-computer rule reads (shared/cloud-computer.ts). */
6094
- function cloudEngine(instance) {
6095
- return { driverKind: instance?.driverKind, computerMcp: instance?.adapter.capabilities.computerMcp };
6096
- }
6097
- /** How a turn's engine reaches a cloud computer. The Computer engine runs its
6098
- * whole turn on the Boat. Every other engine keeps its own model and sign-in
6099
- * and gets the Boat as one more stdio computer server (harness-mcp-proxy
6100
- * computer), in the same slot a Local VM or VPS uses: one mechanism on a
6101
- * desktop, a headless server and an OMB Cloud. The agent process holds only a
6102
- * turn-scoped capability naming this Boat, never a Boat credential. */
6103
- function cloudComputerMount(botId, owner, boxId, remoteAgent) {
6104
- if (remoteAgent)
6105
- return { computer: { kind: "box", boxId } };
6225
+ /** The engine fact the cloud-computer rule reads (shared/cloud-computer.ts),
6226
+ * and the model's name for a refusal to say. */
6227
+ function cloudEngine(instance, model) {
6228
+ return {
6229
+ computerMcp: instance?.adapter.capabilities.computerMcp,
6230
+ name: model?.trim() || instance?.displayName || instance?.driverKind || "This model",
6231
+ };
6232
+ }
6233
+ /** How a turn's engine reaches a cloud computer. Every engine keeps its own
6234
+ * model and sign-in and gets the Boat as one more stdio computer server
6235
+ * (harness-mcp-proxy computer), in the same slot a Local VM or VPS uses: one
6236
+ * mechanism on a desktop, a headless server and an OMB Cloud. The agent
6237
+ * process holds only a turn-scoped capability naming this Boat, never a Boat
6238
+ * credential. */
6239
+ function cloudComputerMount(botId, owner, boxId) {
6240
+ return { localComputer: cloudComputerTools(botId, owner, boxId) };
6241
+ }
6242
+ /** The cloud computer's stdio tools for this turn. `boxId` null: the Boat is
6243
+ * the one the first computer call's claim creates or wakes (mountBotBoat). */
6244
+ function cloudComputerTools(botId, owner, boxId) {
6106
6245
  const control = controlIntegration(botId, owner.threadId, owner.generation, { boxId });
6107
6246
  return {
6108
- localComputer: {
6109
- command: process.execPath,
6110
- args: [SPAWNED_PROXIES.harnessMcp, "computer"],
6111
- env: { ...AGENTS_NODE_FLAG, OMB_HARNESS_URL: `http://127.0.0.1:${PORT}`, OMB_MCP_TOKEN: control.token },
6112
- platform: "linux",
6113
- },
6247
+ command: process.execPath,
6248
+ args: [SPAWNED_PROXIES.harnessMcp, "computer"],
6249
+ env: { ...AGENTS_NODE_FLAG, OMB_HARNESS_URL: `http://127.0.0.1:${PORT}`, OMB_MCP_TOKEN: control.token },
6250
+ platform: "linux",
6114
6251
  };
6115
6252
  }
6116
6253
  /** "Works on: This computer", strict. One mount for a bot thread and a group
@@ -6137,8 +6274,12 @@ async function mountHostComputer(owner, botId, providerSupportsLocal) {
6137
6274
  * is claimed on the first computer call, through the computer-control gate
6138
6275
  * (the Local VM's seam, #1361), never at mount: turns that never touch the
6139
6276
  * computer tools run side by side. `start` may provision or start the
6140
- * container; without it the VPS is only inspected. The caller has already
6141
- * recorded the thread with vpsThreadStarted and ends it on a problem. */
6277
+ * container; without it the VPS is only inspected. Unlike the bot's cloud
6278
+ * computer (mountBotBoat), the start stays here, at dispatch: the VPS tools
6279
+ * are a bridge that execs into the running container as soon as the engine
6280
+ * launches it (runMcpBridge), and a self-hosted container uses no plan
6281
+ * hours. The caller has already recorded the thread with vpsThreadStarted
6282
+ * and ends it on a problem. */
6142
6283
  async function mountBotVps(bot, owner, opts) {
6143
6284
  const vpsResource = `computer:vps:${vpsSshAlias(cfg)}:${bot.id}`;
6144
6285
  const remote = opts.start
@@ -6169,54 +6310,113 @@ async function mountBotVps(bot, owner, opts) {
6169
6310
  },
6170
6311
  };
6171
6312
  }
6172
- /** The bot's own Boat. Explicit Cloud is the consent boundary: it may create a
6173
- * missing machine and wake an archived one (~8s, and it un-pauses billing);
6174
- * Auto (only ever the Computer engine, see startTurn) only attaches a machine
6175
- * that is already running. Callers have already checked that the engine can
6176
- * work on it (cloudPlaceDriverError). */
6177
- async function attachBotBoat(bot, owner, opts) {
6178
- // Explicit cloud turns can provision/wake the same bot's Boat. Claim before
6179
- // any network await so setup itself cannot race another turn.
6180
- if (opts.explicitCloud)
6181
- await bindTurnComputer(owner, `computer:box-bot:${bot.id}`, true);
6182
- let b;
6183
- try {
6184
- b = await boat.findBoat(cfg, bot.id);
6185
- }
6186
- catch (error) {
6187
- // Auto may fall through when an optional provider is offline, but a
6188
- // durable deletion fence must never be mistaken for "no computer".
6189
- if (opts.explicitCloud || error?.status === 409)
6190
- throw error;
6191
- b = null;
6192
- }
6193
- const lifecycleOf = (explicitCloud) => boat.boatTurnLifecycleAction({
6194
- explicitCloud,
6195
- state: typeof b?.state === "string" ? b.state : null,
6196
- });
6197
- let lifecycle = lifecycleOf(opts.explicitCloud);
6313
+ /** How long one computer call waits on a cloud computer that is being
6314
+ * created or woken before it is told to look again. Under the 60 seconds an
6315
+ * engine may give one tool call (Codex's default); a call that runs out finds
6316
+ * the start still going and waits again, inside the start's own budget.
6317
+ * OMB_CLOUD_COMPUTER_START_WAIT_MS shortens it for tests. */
6318
+ const CLOUD_COMPUTER_START_WAIT_MS = Math.max(1_000, Number(process.env.OMB_CLOUD_COMPUTER_START_WAIT_MS) || 45_000);
6319
+ /** How long the start itself may take once this turn has the computer to
6320
+ * itself: above a slow first start (about a minute; Boat's own ready wait is
6321
+ * 90 seconds). Past it the start has failed, and the turn says so and ends,
6322
+ * instead of every call answering "still starting" until the watchdog.
6323
+ * OMB_CLOUD_COMPUTER_START_BUDGET_MS shortens it for tests. */
6324
+ const CLOUD_COMPUTER_START_BUDGET_MS = Math.max(1_000, Number(process.env.OMB_CLOUD_COMPUTER_START_BUDGET_MS) || 150_000);
6325
+ const CLOUD_COMPUTER_STARTING = "The cloud computer is still starting. This call was not performed. Take a fresh screenshot in a moment.";
6326
+ const CLOUD_COMPUTER_DID_NOT_START = "the cloud computer didn't start in time";
6327
+ /** The bot's own Boat as a computer its engine uses, mounted like the VPS
6328
+ * (mountBotVps): the tools mount now, and the Boat is created or woken by
6329
+ * the turn's first computer call, through the computer-control gate (the
6330
+ * Local VM's seam, #1361), never at mount. A turn that never touches the
6331
+ * screen ("hi") makes no Boat call at all, so it costs no plan hours. The
6332
+ * claim is attachBotBoat itself, with its explicit-Cloud consent: the person
6333
+ * chose this place, and the bot is now using it. */
6334
+ function mountBotBoat(bot, owner, opts) {
6335
+ const slot = {
6336
+ owner,
6337
+ lazy: true,
6338
+ startsComputer: true,
6339
+ label: "the cloud computer",
6340
+ graceMs: CLOUD_COMPUTER_START_WAIT_MS,
6341
+ // Until this turn has the computer to itself another conversation does,
6342
+ // and a call is told so (the gate's contention text); from then on the
6343
+ // claim is starting it.
6344
+ onRejected: opts.onRejected,
6345
+ claim: async () => {
6346
+ let attached;
6347
+ try {
6348
+ attached = await attachBotBoat(bot, owner, {
6349
+ budgetMs: CLOUD_COMPUTER_START_BUDGET_MS,
6350
+ onSeated: () => { slot.pendingReason = CLOUD_COMPUTER_STARTING; },
6351
+ });
6352
+ if (!attached)
6353
+ throw new Error("the cloud computer could not be created or reached");
6354
+ }
6355
+ catch (error) {
6356
+ throw opts.source ? await asPlaceFailure(error, "cloud", opts.source, bot, "box") : error;
6357
+ }
6358
+ slot.boxId = attached.boxId;
6359
+ opts.onClaimed(attached.capture);
6360
+ },
6361
+ };
6362
+ autoVmClaims.set(owner.threadId, slot);
6363
+ return cloudComputerTools(bot.id, owner, null);
6364
+ }
6365
+ /** The Boat a lazily mounted cloud computer's claim landed on, for this
6366
+ * exact turn; none while the claim is still running or after it failed. */
6367
+ function claimedBoatId(capability) {
6368
+ const slot = autoVmClaims.get(capability.threadId);
6369
+ return slot?.owner.generation === capability.generation && slot.claimed ? slot.boxId : undefined;
6370
+ }
6371
+ /** The bot's own Boat, for a turn whose place is Cloud. That choice is the
6372
+ * consent boundary: it may create a missing machine and wake an archived one
6373
+ * (~8s, and it un-pauses billing). Auto never reaches here. Callers have
6374
+ * already checked that the engine can work on it (cloudPlaceRefusal).
6375
+ * Every engine reaches it through mountBotBoat, on its first computer call:
6376
+ * told once this turn has the Boat to itself, it has `budgetMs` to find,
6377
+ * create or wake it. */
6378
+ async function attachBotBoat(bot, owner, start) {
6379
+ // Claim before any network await so setup itself cannot race another turn.
6380
+ await bindTurnComputer(owner, `computer:box-bot:${bot.id}`, true);
6381
+ start.onSeated();
6382
+ const machine = await withinBudget(startBotBoat(bot), start.budgetMs, CLOUD_COMPUTER_DID_NOT_START);
6383
+ if (!machine)
6384
+ return null;
6385
+ await bindTurnComputer(owner, `computer:box:${machine.id}`);
6386
+ return {
6387
+ boxId: machine.id,
6388
+ capture: () => boat.screenshotBoat(cfg, bot.id, machine.id),
6389
+ };
6390
+ }
6391
+ /** `work`, or a rejection with `message` once `ms` pass first. The work is
6392
+ * not cancelled: it ends on its own requests' deadlines, and the race has
6393
+ * already handled a rejection it brings later. */
6394
+ function withinBudget(work, ms, message) {
6395
+ let timer;
6396
+ const expired = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); });
6397
+ return Promise.race([work, expired]).finally(() => clearTimeout(timer));
6398
+ }
6399
+ /** Find the bot's Boat, creating or waking it: the Boat once it can be
6400
+ * attached, or null. */
6401
+ async function startBotBoat(bot) {
6402
+ let b = await boat.findBoat(cfg, bot.id);
6403
+ const lifecycleOf = () => boat.boatTurnLifecycleAction(typeof b?.state === "string" ? b.state : null);
6404
+ let lifecycle = lifecycleOf();
6198
6405
  if (lifecycle === "provision") {
6199
- broadcast({ kind: "computer", botId: bot.id, state: "provisioning" });
6406
+ broadcast({ kind: "computer", botId: bot.id, state: "provisioning", place: "cloud" });
6200
6407
  await boat.provisionBoat(cfg, bot.id, bot.name);
6201
6408
  b = await boat.findBoat(cfg, bot.id);
6202
- lifecycle = lifecycleOf(true);
6409
+ lifecycle = lifecycleOf();
6203
6410
  }
6204
6411
  // an archived boat answers every action with an error until it resumes —
6205
6412
  // wake it here, once, instead of letting the agent discover it one failed
6206
6413
  // tool call at a time.
6207
6414
  if (lifecycle === "wake") {
6208
- broadcast({ kind: "computer", botId: bot.id, state: "waking" });
6415
+ broadcast({ kind: "computer", botId: bot.id, state: "waking", place: "cloud" });
6209
6416
  b = (await boat.readyBoat(cfg, bot.id)) ?? b;
6210
- lifecycle = lifecycleOf(true);
6417
+ lifecycle = lifecycleOf();
6211
6418
  }
6212
- if (!b || lifecycle !== "attach")
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
- };
6419
+ return b && lifecycle === "attach" ? b : null;
6220
6420
  }
6221
6421
  function managedBoatOwners() {
6222
6422
  return [...store.bots.map((bot) => ({
@@ -6288,8 +6488,6 @@ function turnProvider(bot, runOn, threadId) {
6288
6488
  const wants = turnSurfacePlan(bot, runOn, threadId).computer;
6289
6489
  if (wants !== undefined && wants !== "cloud")
6290
6490
  return null;
6291
- if (registry.get(bot.modelSelection.instanceId)?.adapter.capabilities.remoteAgent === true)
6292
- return "box";
6293
6491
  return bot.cloudBackend === "vps" ? "vps" : wants === "cloud" ? "box" : null;
6294
6492
  }
6295
6493
  /** Preview requests carry the selected conversation, not whichever thread
@@ -6308,8 +6506,6 @@ async function computerPreviewSurface(bot, threadId) {
6308
6506
  if (plan.computer !== undefined)
6309
6507
  return plan.computer === "off" && plan.browser ? "browser" : plan.computer;
6310
6508
  const instance = registry.get(bot.modelSelection.instanceId);
6311
- if (instance?.adapter.capabilities.remoteAgent === true)
6312
- return "cloud";
6313
6509
  if (bot.cloudBackend === "vps") {
6314
6510
  const remote = await vps.vpsComputerStatus(cfg, bot.id);
6315
6511
  if (remote.ready)
@@ -6337,7 +6533,8 @@ async function selectableComputers(bot) {
6337
6533
  const instance = registry.get(bot.modelSelection.instanceId);
6338
6534
  const caps = instance?.adapter.capabilities;
6339
6535
  const off = bot.computer === "off";
6340
- const localEngine = caps?.remoteAgent !== true;
6536
+ // A desktop the bot's tools cannot reach is not offered, and not probed.
6537
+ const toolsLeaveOut = computerToolsRefusal(bot.toolScope, "works-on", bot.name)?.message;
6341
6538
  const surfaces = ["cloud", "vm", "local", "browser"].filter(surface => !CLOUD_HOME || cloudHomeOffersPlace(surface));
6342
6539
  return Promise.all(surfaces.map(async (surface) => {
6343
6540
  let ready = false;
@@ -6351,24 +6548,31 @@ async function selectableComputers(bot) {
6351
6548
  try {
6352
6549
  if (off)
6353
6550
  reason = "Computer access is Off in this bot's settings.";
6551
+ else if (toolsLeaveOut && surface !== "browser")
6552
+ reason = toolsLeaveOut;
6354
6553
  else if (surface === "cloud") {
6355
6554
  if (bot.cloudBackend === "vps") {
6356
- const status = canWorkOnCloud(cloudEngine(instance), "vps") ? await vps.vpsComputerStatus(cfg, bot.id) : null;
6555
+ const status = canWorkOnCloud(cloudEngine(instance)) ? await vps.vpsComputerStatus(cfg, bot.id) : null;
6357
6556
  ready = status?.ready === true;
6358
6557
  canStart = Boolean(status?.daemonUp && status.managed && status.container === "stopped" &&
6359
6558
  status.image && status.imageMatches && status.network === "private" && status.mounts === "none" && status.security === "hardened");
6360
6559
  canCreate = Boolean(status?.configured && status.daemonUp && status.container === "missing");
6361
6560
  reason = status?.problem ?? reason;
6362
6561
  }
6363
- else if (boat.boatConfigured(cfg) && canWorkOnCloud(cloudEngine(instance), "box")) {
6562
+ else if (boat.boatConfigured(cfg) && canWorkOnCloud(cloudEngine(instance))) {
6364
6563
  const status = await boat.boatStatus(cfg, bot.id);
6365
- const lifecycle = boat.boatTurnLifecycleAction({ explicitCloud: true, state: status.box?.state ?? null });
6564
+ const lifecycle = boat.boatTurnLifecycleAction(status.box?.state ?? null);
6366
6565
  ready = lifecycle === "attach";
6367
6566
  canStart = lifecycle === "wake";
6368
6567
  canCreate = lifecycle === "provision";
6369
6568
  }
6569
+ else {
6570
+ // The words the person reads for the same place (shared/place-view.ts).
6571
+ reason = (cloudPlaceRefusal(cloudEngine(instance, bot.modelSelection.model), "works-on", bot.name)
6572
+ ?? boatNotConfigured(bot, "works-on")).message;
6573
+ }
6370
6574
  }
6371
- else if (surface === "vm" && localEngine && caps?.computerMcp) {
6575
+ else if (surface === "vm" && caps?.computerMcp) {
6372
6576
  const target = localVmTargetForStatus(bot.id);
6373
6577
  const status = await containerComputerStatus(undefined, undefined, target);
6374
6578
  ready = status.ready;
@@ -7881,12 +8085,15 @@ function finalizeDelegationWatch(threadId, ok, reply = "", failureName = "Delega
7881
8085
  // with the reply only visible in the thread (the "delegated and went
7882
8086
  // silent" gap). Failures wake it too — the user must hear the task did
7883
8087
  // not finish. Idle-checked and burst-capped so a busy source or a
7884
- // re-delegating loop cannot spin up runs.
7885
- if (ok) {
7886
- wakeDelegationSource(source, watched.sourceThreadId, targetName, undefined, watched.routineRunId);
7887
- }
7888
- else if (!ok) {
7889
- wakeDelegationSource(source, watched.sourceThreadId, targetName, failureName || "the delegated turn did not finish", watched.routineRunId);
8088
+ // re-delegating loop cannot spin up runs. An external runtime polls
8089
+ // for the receipt instead; waking would answer it a second time.
8090
+ if (watched.completionOwner !== "external") {
8091
+ if (ok) {
8092
+ wakeDelegationSource(source, watched.sourceThreadId, targetName, undefined, watched.routineRunId);
8093
+ }
8094
+ else {
8095
+ wakeDelegationSource(source, watched.sourceThreadId, targetName, failureName || "the delegated turn did not finish", watched.routineRunId);
8096
+ }
7890
8097
  }
7891
8098
  }
7892
8099
  }
@@ -7941,7 +8148,7 @@ bus.subscribe((event) => {
7941
8148
  /** How a drained delegation becomes a real turn on the target. Shared by
7942
8149
  * the settle-time drain and the boot-time drain of what a previous process
7943
8150
  * left queued. */
7944
- const runDelegatedTurn = (toBotId, rawText, commsDepth, sourceThreadId, channel, taskId, sourceBotId, openedThreadId, oneWay) => {
8151
+ const runDelegatedTurn = (toBotId, rawText, commsDepth, sourceThreadId, channel, taskId, sourceBotId, openedThreadId, oneWay, completionOwner) => {
7945
8152
  // startTurn REJECTS on an ordinary condition — busy target, deleted bot,
7946
8153
  // unavailable provider. Unhandled, that rejection is fatal to the
7947
8154
  // harness (Node's default), which in the packaged app kills the server
@@ -7975,6 +8182,7 @@ const runDelegatedTurn = (toBotId, rawText, commsDepth, sourceThreadId, channel,
7975
8182
  routineRunId: activeRoutineRunForThread(sourceThreadId)?.id,
7976
8183
  startedAtMs: Date.now(),
7977
8184
  ...(oneWay ? { oneWay: true } : {}),
8185
+ ...(completionOwner ? { completionOwner } : {}),
7978
8186
  });
7979
8187
  }
7980
8188
  let failureReported = false;
@@ -8469,10 +8677,9 @@ const screenPollers = new Map();
8469
8677
  const SCREEN_POLL_MS = 6000;
8470
8678
  const SCREEN_MIN_GAP_MS = 3000;
8471
8679
  const SCREEN_SETTLE_TIMEOUT_MS = 10_000;
8472
- /** `screenIsTheWork` starts the turn already counting as screen usage: a
8473
- * boxAgent's whole session runs ON the boat, so every tool it calls acts on
8474
- * that screen even though none of them is named like a computer tool. Its
8475
- * shell-only turns are kept honest by the settle-time hash gate instead. */
8680
+ /** `screenIsTheWork` starts the poller already counting as screen usage: a
8681
+ * poller restarted mid-turn (a lazy claim swapping in its capture) carries
8682
+ * over whether the turn had touched its screen. */
8476
8683
  function startScreenPoller(botId, threadId, captures, { screenIsTheWork = false } = {}) {
8477
8684
  if (!captures.computer && !captures.browser)
8478
8685
  return;
@@ -8564,10 +8771,9 @@ function shownScreenHash(threadId) {
8564
8771
  * end state, not the previous action's. A turn that never touched the
8565
8772
  * screen settles nothing — and skips the capture, which is one less
8566
8773
  * command on the boat's single endpoint. A frame the reader can already see
8567
- * settles nothing either: the boxAgent pre-touch counts every turn as
8568
- * screen work, so without this its shell-only replies would all end in the
8569
- * same idle desktop. Either way the poller is torn down here, so no
8570
- * per-turn state survives the turn. */
8774
+ * settles nothing either, so a turn never ends on the same idle desktop it
8775
+ * already showed. Either way the poller is torn down here, so no per-turn
8776
+ * state survives the turn. */
8571
8777
  async function finalScreenFrame(_botId, threadId) {
8572
8778
  const entry = screenPollers.get(threadId);
8573
8779
  const owner = turnResourceOwners.get(threadId);
@@ -8695,6 +8901,10 @@ async function startTurn(botId, text, opts) {
8695
8901
  const plan = turnSurfacePlan(bot, opts?.runOn, threadId);
8696
8902
  const instance = registry.get(bot.modelSelection.instanceId);
8697
8903
  if (!instance) {
8904
+ // Still on the removed Computer engine: its move failed or found no
8905
+ // engine. Try it again now, so the next message runs on the new engine.
8906
+ if (removedComputerInstanceIds(cfg.instances).has(bot.modelSelection.instanceId))
8907
+ void retryComputerEngineMove();
8698
8908
  throw Object.assign(new Error(`provider instance "${bot.modelSelection.instanceId}" is unavailable — pick another model in settings`), { status: 409 });
8699
8909
  }
8700
8910
  const policyRefusal = policyModelRefusal(instance);
@@ -8988,7 +9198,11 @@ async function startTurn(botId, text, opts) {
8988
9198
  const userTurnText = promptWithReply(skillAuthoring ? expandLearnTurnText(setupText) : setupText, opts?.replyTo, cfg.profile?.name?.trim() || "User");
8989
9199
  // Decided again at dispatch when setup outlasted a soul edit (config).
8990
9200
  const decideContext = (config) => {
8991
- const handedStale = Boolean(handed && (!unseen || (externalUpdate && handed.config !== config)));
9201
+ // A Codex thread keeps the last effort it was sent and no turn can clear
9202
+ // one: back on the default, a thread that holds a level is rebuilt from
9203
+ // the transcript rather than resumed on that level.
9204
+ const effortHeld = !effort && typeof handed?.effort === "string";
9205
+ const handedStale = Boolean(handed && (!unseen || effortHeld || (externalUpdate && handed.config !== config)));
8992
9206
  const { block: unseenBlock, placed } = unseen && !handedStale ? renderUnseen(unseen) : { block: "", placed: [] };
8993
9207
  const { turnText: contextTurnText, resume } = buildTurnContext({
8994
9208
  text: userTurnText,
@@ -9021,6 +9235,7 @@ async function startTurn(botId, text, opts) {
9021
9235
  resumeCursor, sessionReset: !resume, recoveryText, recoveryIsReplay,
9022
9236
  handoff: strictResume ? {
9023
9237
  botId: bot.id, instanceId, config, resumeCursor: typeof resumeCursor === "string" ? resumeCursor : undefined,
9238
+ ...(instance.driverKind === "codex" ? { effort: effort ?? null } : {}),
9024
9239
  started: sessionStart(contextOrder, contextTurnText !== userTurnText ? windowIds : [], carried),
9025
9240
  recovery: sessionStart(contextOrder, recoveryText !== undefined ? windowIds : [], carried),
9026
9241
  resumed: sessionStart(contextOrder, [], [...placed, ...carried]),
@@ -9093,8 +9308,7 @@ async function startTurn(botId, text, opts) {
9093
9308
  // because moving a live session would break resume.
9094
9309
  // A cloud run keeps the bot's own engine on this host, with the cloud
9095
9310
  // computer as one more tool, so it works in the same folder as any
9096
- // other turn (only the Computer engine runs on the Boat, and it has no
9097
- // workspace files).
9311
+ // other turn.
9098
9312
  // On a Cloud home a conversation a guest opened is pinned to its own
9099
9313
  // folder when it first runs, not the bot's project folder the owner's
9100
9314
  // conversations share (what it leaves there reaches none of them). One
@@ -9135,9 +9349,6 @@ async function startTurn(botId, text, opts) {
9135
9349
  // landed. A team computer or a cloud routine is not this conversation's
9136
9350
  // choice, so those ignore the pin.
9137
9351
  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
9352
  const wants = plan.computer;
9142
9353
  // A place the organisation disallows, or a Cloud home never offers, is
9143
9354
  // refused before anything is prepared; Auto below simply skips them.
@@ -9154,12 +9365,14 @@ async function startTurn(botId, text, opts) {
9154
9365
  : opts?.runOn === "cloud" ? "routine"
9155
9366
  : plan.pinned ? (dispatchTask?.surfaceSource === "auto" ? "auto-pin" : "pin")
9156
9367
  : "works-on";
9157
- // An engine that cannot use the cloud computer is refused here, before
9158
- // anything is created or woken — never handed to another engine.
9368
+ // An engine that cannot use the cloud computer, or a Tool selection
9369
+ // that leaves its tools out, is refused here, before anything is
9370
+ // created or woken — never handed to another engine.
9159
9371
  if (wants === "cloud" && placeSource) {
9160
- const unsupported = cloudPlaceDriverError(cloudEngine(instance), cloudBackend, placeSource);
9372
+ const unsupported = cloudPlaceRefusal(cloudEngine(instance, bot.modelSelection.model), placeSource, bot.name)
9373
+ ?? computerToolsRefusal(toolScope, placeSource, bot.name);
9161
9374
  if (unsupported)
9162
- throw new PlaceUnavailableError("cloud", unsupported);
9375
+ throw unsupported;
9163
9376
  }
9164
9377
  let previewCapture = null;
9165
9378
  let browserCapture = null;
@@ -9285,7 +9498,7 @@ async function startTurn(botId, text, opts) {
9285
9498
  // the newer turn. The gate's refusals stay fail-closed for any screen
9286
9499
  // call racing this teardown; the turn-failed buzz and incident follow
9287
9500
  // the dispatch-failure rules (person-started turns only).
9288
- const surfaceLazyClaimRejection = (label) => (failure) => {
9501
+ const surfaceLazyClaimRejection = (label) => (failure, error) => {
9289
9502
  if (activeInternalGenerationByThread.get(threadId) !== resourceOwner.generation || !threadBusy(bot.id, threadId))
9290
9503
  return;
9291
9504
  // The wait ceiling parked this turn (#1651): register the resume and
@@ -9304,11 +9517,16 @@ async function startTurn(botId, text, opts) {
9304
9517
  void interruptDirectThread(bot.id, threadId, { preserveComputerResume: true }).catch(() => { });
9305
9518
  return;
9306
9519
  }
9307
- const message = `computer unavailable — ${label} could not be claimed for this turn (${failure})`;
9520
+ // A place that failed (mountBotBoat) is already read as its one line
9521
+ // and one way on, and never sticks as an Auto-recorded pin.
9522
+ const placeFailure = error instanceof PlaceUnavailableError ? error : undefined;
9523
+ if (placeFailure)
9524
+ clearFailedAutoPin(bot.id, threadId, placeFailure.place);
9525
+ const message = placeFailure?.message ?? `computer unavailable — ${label} could not be claimed for this turn (${failure})`;
9308
9526
  store.appendMessage(threadId, {
9309
9527
  role: "bot",
9310
9528
  kind: "activity",
9311
- tool: failedTurnTool(message),
9529
+ tool: failedTurnTool(message, placeFailure ? { place: placeFailure.row } : {}),
9312
9530
  });
9313
9531
  if (opts?.automationSource === undefined && !opts?.commsDepth && !opts?.cardContinuation) {
9314
9532
  notify(buildNotification("turn-failed", bot, threadId, redactSecretsInText(message), { avatarUrl: bot.avatarUrl }));
@@ -9317,8 +9535,26 @@ async function startTurn(botId, text, opts) {
9317
9535
  // completion fold must not report this failure a second time.
9318
9536
  resourceOwner.lazyClaimFailureReported = true;
9319
9537
  }
9538
+ // A routine, a delegation or a resume reports this exactly as it
9539
+ // reports a place that failed at dispatch: with this cause, before
9540
+ // the interrupt settles the turn as merely "interrupted".
9541
+ opts?.onDispatchError?.(message);
9320
9542
  void interruptDirectThread(bot.id, threadId).catch(() => { });
9321
9543
  };
9544
+ /** A lazily claimed remote desktop (the VPS, the bot's Boat) streams
9545
+ * from the moment its claim lands. The dispatch-site poller start saw
9546
+ * no capture (the claim runs after dispatch), so restart the poller
9547
+ * with this one, keeping any browser capture and whether this turn
9548
+ * already touched its screen. Same still-running guard as dispatch: a
9549
+ * poller started after its own turn.completed would never stop. */
9550
+ const adoptClaimedCapture = (capture) => {
9551
+ previewCapture = capture;
9552
+ if (!threadBusy(bot.id, threadId))
9553
+ return;
9554
+ const touched = screenPollers.get(threadId)?.touched ?? false;
9555
+ stopScreenPoller(bot.id, threadId);
9556
+ startScreenPoller(bot.id, threadId, { computer: capture, ...(browserCapture ? { browser: browserCapture } : {}) }, { screenIsTheWork: touched });
9557
+ };
9322
9558
  // Explicit destinations are strict. In particular, Local VM must never
9323
9559
  // fall through to host CUA and accidentally click on the user's Mac.
9324
9560
  // The Local VM attach, shared by explicit "Local VM" and by Auto. Explicit
@@ -9327,7 +9563,7 @@ async function startTurn(botId, text, opts) {
9327
9563
  // be recreated on demand after idling away — and never creates a first
9328
9564
  // VM on its own; every failure there is a quiet "not this place".
9329
9565
  const attachLocalVm = async (strict) => {
9330
- if (!mountsComputerMcp || instance.adapter.capabilities.remoteAgent === true) {
9566
+ if (!mountsComputerMcp) {
9331
9567
  if (!strict)
9332
9568
  return false;
9333
9569
  throw new Error("this model cannot use the Local VM — choose Claude or an ACP model provider, or select another computer destination");
@@ -9383,7 +9619,7 @@ async function startTurn(botId, text, opts) {
9383
9619
  // guard as dispatch: a poller started after its own
9384
9620
  // turn.completed would never be torn down.
9385
9621
  if (previewCapture && threadBusy(bot.id, threadId)) {
9386
- const touched = screenPollers.get(threadId)?.touched ?? instance.adapter.capabilities.remoteAgent === true;
9622
+ const touched = screenPollers.get(threadId)?.touched ?? false;
9387
9623
  stopScreenPoller(bot.id, threadId);
9388
9624
  startScreenPoller(bot.id, threadId, { computer: previewCapture, ...(browserCapture ? { browser: browserCapture } : {}) }, { screenIsTheWork: touched });
9389
9625
  }
@@ -9430,7 +9666,7 @@ async function startTurn(botId, text, opts) {
9430
9666
  // the person explicitly opted this bot into remote lifecycle actions.
9431
9667
  // On Auto an engine that cannot use a computer simply skips it.
9432
9668
  if ((wants === "cloud" || (wants === undefined && managedPolicy.computerAllowed("vps"))) && cloudBackend === "vps" &&
9433
- canWorkOnCloud(cloudEngine(instance), "vps")) {
9669
+ canWorkOnCloud(cloudEngine(instance))) {
9434
9670
  // The VPS "computer" is the desktop inside this bot's managed
9435
9671
  // container, and only screen work needs that desktop to itself.
9436
9672
  // So the lease is claimed on the first computer call, through the
@@ -9447,12 +9683,7 @@ async function startTurn(botId, text, opts) {
9447
9683
  // Local VM's lazy claim does.
9448
9684
  onClaimed: (vpsCapture) => {
9449
9685
  pinAutoSurface("cloud");
9450
- previewCapture = vpsCapture;
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
- }
9686
+ adoptClaimedCapture(vpsCapture);
9456
9687
  },
9457
9688
  onRejected: surfaceLazyClaimRejection("the VPS computer"),
9458
9689
  });
@@ -9469,30 +9700,31 @@ async function startTurn(botId, text, opts) {
9469
9700
  }
9470
9701
  }
9471
9702
  if (teamComputer) {
9472
- const attached = await attachTeamBoat(teamComputer, bot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance), "box"), instance.adapter.capabilities.remoteAgent === true);
9703
+ const attached = await attachTeamBoat(teamComputer, bot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance)));
9473
9704
  Object.assign(integrations, attached.mount);
9474
9705
  previewCapture = attached.capture;
9475
9706
  computerKind = "box";
9476
9707
  }
9477
- // Cloud is strict when selected. On Auto only the Computer engine,
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
9708
+ // Cloud is strict when selected. A turn reaches the Boat only when
9480
9709
  // this conversation is on it (Works on, a pin, a cloud routine, or
9481
9710
  // select_computer): an Auto turn never reads the Boat account, so it
9482
9711
  // adds no Boat call, cannot be stopped by a Boat being deleted, and
9483
- // never records a place nobody chose.
9484
- const remoteAgent = instance.adapter.capabilities.remoteAgent === true;
9485
- if (!teamComputer && cloudBackend === "box" && boat.boatConfigured(cfg) &&
9486
- (wants === "cloud" || (wants === undefined && remoteAgent && !computerPlaceRefusal("box")))) {
9487
- const attached = await attachBotBoat(bot, resourceOwner, { explicitCloud: wants === "cloud", remoteAgent });
9488
- if (attached) {
9489
- previewCapture = attached.capture;
9490
- Object.assign(integrations, attached.mount);
9491
- computerKind = "box";
9492
- }
9712
+ // never records a place nobody chose. Even then the Boat is created
9713
+ // or woken only by the bot's first computer call (mountBotBoat): a
9714
+ // chat that never touches the screen uses no plan hours.
9715
+ if (!teamComputer && cloudBackend === "box" && boat.boatConfigured(cfg) && wants === "cloud") {
9716
+ integrations.localComputer = mountBotBoat(bot, resourceOwner, {
9717
+ // The row a failed attach always wrote: the place's one line and
9718
+ // its one way on, read where the place came from (an
9719
+ // Auto-recorded pin is cleared, as dispatch clears it).
9720
+ source: placeSource,
9721
+ onClaimed: adoptClaimedCapture,
9722
+ onRejected: surfaceLazyClaimRejection("the cloud computer"),
9723
+ });
9724
+ computerKind = "box";
9493
9725
  }
9494
9726
  if (wants === "cloud" && cloudBackend === "box" && !boat.boatConfigured(cfg)) {
9495
- throw new Error(BOAT_NOT_CONFIGURED);
9727
+ throw boatNotConfigured(bot, placeSource ?? "works-on");
9496
9728
  }
9497
9729
  if (wants === "cloud" && cloudBackend === "box" && computerKind !== "box") {
9498
9730
  throw new Error("the cloud computer could not be created or reached");
@@ -9503,21 +9735,20 @@ async function startTurn(botId, text, opts) {
9503
9735
  // they are.
9504
9736
  if (!namedPlace || !placeSource)
9505
9737
  throw error;
9506
- throw asPlaceFailure(error, namedPlace, placeSource);
9738
+ throw await asPlaceFailure(error, namedPlace, placeSource, bot, cloudBackend);
9507
9739
  }
9508
9740
  // Auto-only host fallback. Electron owns cua-driver/TCC attribution;
9509
9741
  // the harness only reads its already-running connection descriptor.
9510
9742
  // Auto reaches a Local VM this bot already has before it ever touches the
9511
9743
  // host's own desktop: on a headless server that VM is the only desktop
9512
9744
  // there is, and a person who prepared one meant it to be used.
9513
- if (wants === undefined && !integrations.computer && !integrations.localComputer && !computerPlaceRefusal("localVm") && await attachLocalVm(false))
9745
+ if (wants === undefined && !integrations.localComputer && !computerPlaceRefusal("localVm") && await attachLocalVm(false))
9514
9746
  computerKind = "vm";
9515
9747
  // An unattended run on a bot with a VPS configured never lands on the
9516
9748
  // host's own desktop instead: a scheduled job clicking on someone's
9517
9749
  // laptop is worse than a scheduled job that fails and says why.
9518
9750
  const unattendedVps = cloudBackend === "vps" && Boolean(opts?.automationSource);
9519
- if (!integrations.computer &&
9520
- !integrations.localComputer &&
9751
+ if (!integrations.localComputer &&
9521
9752
  wants === undefined &&
9522
9753
  !unattendedVps &&
9523
9754
  !computerPlaceRefusal("thisComputer") &&
@@ -9552,7 +9783,6 @@ async function startTurn(botId, text, opts) {
9552
9783
  }
9553
9784
  if (wants === undefined &&
9554
9785
  cloudBackend === "vps" &&
9555
- !integrations.computer &&
9556
9786
  !integrations.localComputer &&
9557
9787
  autoVpsProblem &&
9558
9788
  !computerSelectionTurns.has(threadId)) {
@@ -9560,7 +9790,7 @@ async function startTurn(botId, text, opts) {
9560
9790
  ? "This scheduled run tried to start the VPS computer and could not reach it. Check the VPS connection in Settings → API keys."
9561
9791
  : bot.autoStartVps
9562
9792
  ? "Check the VPS connection in Settings → API keys."
9563
- : "Open Computer and enable Start VPS automatically, or choose Cloud to start it manually.";
9793
+ : "Open Computer and enable Start VPS automatically, or choose Cloud computer to start it manually.";
9564
9794
  throw new Error(`${autoVpsProblem}. ${hint}`);
9565
9795
  }
9566
9796
  // Agent control tools include peer comms and the secure credential
@@ -9683,9 +9913,8 @@ async function startTurn(botId, text, opts) {
9683
9913
  // Pin on use (issue #1650): every computer that registers a claim
9684
9914
  // slot — a ready Local VM, the VPS, the host fallback, a VM created
9685
9915
  // for this turn — records its pin inside the claim itself, so a turn
9686
- // that only mounted tools records nothing. The Computer engine's Boat
9687
- // (its whole turn runs there) and the built-in browser (no seat to
9688
- // claim) pin here.
9916
+ // that only mounted tools records nothing. The built-in browser (no
9917
+ // seat to claim) pins here.
9689
9918
  if (autoPinAllowed()) {
9690
9919
  const claimSlot = autoVmClaims.get(threadId);
9691
9920
  const pinsAtClaim = claimSlot?.owner.generation === resourceOwner.generation;
@@ -9706,7 +9935,6 @@ async function startTurn(botId, text, opts) {
9706
9935
  }
9707
9936
  const computerPromptKind = resolveComputerPromptKind({
9708
9937
  kind: computerKind,
9709
- driverKind: instance.driverKind,
9710
9938
  vmPrivate: localVmMode(cfg) === "per-bot",
9711
9939
  });
9712
9940
  const coordinationNode = opts?.coordination ? roomHandoffs.nodes.get(opts.coordination.id) : undefined;
@@ -9723,7 +9951,7 @@ async function startTurn(botId, text, opts) {
9723
9951
  { id: "computer", label: "Computer", text: computerPrompt(computerPromptKind) },
9724
9952
  { id: "team-computer", label: "Team computer", text: teamComputerPrompt(teamComputer) },
9725
9953
  { 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: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
9954
+ { id: "cloud-home", label: "My Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
9727
9955
  // gated on the integration, not the key: the hint only goes to a
9728
9956
  // bot whose driver actually mounted the tools
9729
9957
  { id: "composio", label: "Connected apps", text: integrations.composio ? composioSystemPrompt(liveBot?.connectorTools ?? bot.connectorTools) + describeConnectorScopes((liveBot ?? bot).connectorScopes) : "" },
@@ -9856,7 +10084,7 @@ async function startTurn(botId, text, opts) {
9856
10084
  // keep polling the boat forever, carrying dead per-turn state. busy
9857
10085
  // is flipped false in the fold, so it is the honest "still running".
9858
10086
  if ((previewCapture || browserCapture) && threadBusy(bot.id, threadId)) {
9859
- startScreenPoller(bot.id, threadId, { ...(previewCapture ? { computer: previewCapture } : {}), ...(browserCapture ? { browser: browserCapture } : {}) }, { screenIsTheWork: instance.adapter.capabilities.remoteAgent === true });
10087
+ startScreenPoller(bot.id, threadId, { ...(previewCapture ? { computer: previewCapture } : {}), ...(browserCapture ? { browser: browserCapture } : {}) });
9860
10088
  }
9861
10089
  // An adapter may publish completion synchronously just before its
9862
10090
  // dispatch promise resolves. The event could not use the turn-id map
@@ -9950,16 +10178,10 @@ async function startTurn(botId, text, opts) {
9950
10178
  drainDelegationWakes();
9951
10179
  return;
9952
10180
  }
9953
- // A failed place never sticks. An Auto-recorded pin to it (a
9954
- // select_computer switch, or where an earlier Auto turn landed) is the
9955
- // machine's memory, not a choice: clear it so the next message runs on
9956
- // Auto. A person's pin stays, and the message names the composer.
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
- }
10181
+ // A failed place never sticks; a person's pin stays, and the message
10182
+ // names the composer.
10183
+ if (e instanceof PlaceUnavailableError)
10184
+ clearFailedAutoPin(bot.id, threadId, e.place);
9963
10185
  let message = e instanceof Error ? e.message : String(e);
9964
10186
  const current = store.projectBotForTask(bot.id, threadId);
9965
10187
  const backups = cfg.automaticRecovery?.enabled
@@ -10014,7 +10236,7 @@ async function startTurn(botId, text, opts) {
10014
10236
  role: "bot",
10015
10237
  kind: "activity",
10016
10238
  turnSucceeded: false,
10017
- tool: failedTurnTool(message),
10239
+ tool: failedTurnTool(message, e instanceof PlaceUnavailableError ? { place: e.row } : {}),
10018
10240
  });
10019
10241
  // Worth a buzz for the same reason a routine failure is, and the rule
10020
10242
  // notify.ts encodes: the bot is not working, and the cause is usually
@@ -10275,13 +10497,18 @@ routines = new RoutineManager({
10275
10497
  }
10276
10498
  return groupIsWorking(group) || coordinator.busy ? "busy" : "ready";
10277
10499
  },
10278
- createTask: (botId, title, activate = false, routineId) => {
10500
+ createTask: (botId, title, activate = false, run) => {
10279
10501
  const task = store.createTask(botId, title, activate);
10280
10502
  // On a Cloud home a run's conversation is opened, like its results
10281
10503
  // conversation, by whoever wrote the routine: a run of one that is
10282
10504
  // nobody's is confined to a folder of its own, never the bot's project.
10283
- if (task && CLOUD_HOME && routineId)
10284
- threadStarters.set(task.threadId, routineOpener(routineId));
10505
+ // A webhook's run is the owner's: only their own devices can create,
10506
+ // edit or rotate a webhook, so it works at the bot's own level, in its
10507
+ // folder, as on the desktop. Its payload still never reaches the lent
10508
+ // Mac: cloud-lending.ts refuses every webhook run by its trigger.
10509
+ if (task && CLOUD_HOME && run) {
10510
+ threadStarters.set(task.threadId, run.triggerSource === "webhook" ? CLOUD_OWNER_KEY : routineOpener(run.routineId));
10511
+ }
10285
10512
  // The store's frame announces it. A run's task that stays in the
10286
10513
  // background leaves the open thread alone, so that frame carries no
10287
10514
  // transcript: a whole thread on every scheduled run once passed the phone
@@ -10497,25 +10724,25 @@ async function cloudRoutineReadiness(botId, threadId) {
10497
10724
  if (!boat.boatConfigured(cfg)) {
10498
10725
  return {
10499
10726
  ready: false,
10500
- reason: 'The Boat cloud computer needs a working Boat API key. For the bot’s configured computer, including a self-hosted VPS, set run_on="maus" instead.',
10727
+ reason: `${boatNotConfigured(bot, "routine").message} For the bot's configured computer, including a self-hosted VPS, set run_on="maus" instead.`,
10501
10728
  };
10502
10729
  }
10503
10730
  const instance = registry.get(bot.modelSelection.instanceId);
10504
10731
  if (!instance)
10505
10732
  return { ready: false, reason: "The target bot's model is unavailable. Choose another model in the bot's settings." };
10506
10733
  // The same rule a cloud turn applies, before anything is provisioned.
10507
- const unsupported = cloudPlaceDriverError(cloudEngine(instance), "box", "routine");
10734
+ const unsupported = cloudPlaceRefusal(cloudEngine(instance, bot.modelSelection.model), "routine", bot.name);
10508
10735
  if (unsupported)
10509
- return { ready: false, reason: unsupported };
10736
+ return { ready: false, reason: unsupported.message };
10510
10737
  try {
10511
10738
  if ((await instance.snapshot()).state !== "available") {
10512
- return { ready: false, reason: "The target bot's model is not ready to use the Boat cloud computer." };
10739
+ return { ready: false, reason: "The target bot's model is not ready to use the cloud computer." };
10513
10740
  }
10514
10741
  const teamComputer = inheritedTeamComputer(bot);
10515
10742
  const ownerId = teamComputer ? teamComputerOwner(teamComputer.id) : bot.id;
10516
10743
  const machine = await boat.findBoat(cfg, ownerId);
10517
10744
  if (teamComputer && !machine) {
10518
- return { ready: false, reason: "The team's Boat computer is missing; explicitly create or retry it from the Team map." };
10745
+ return { ready: false, reason: "The team's cloud computer is missing; explicitly create or retry it from the Team map." };
10519
10746
  }
10520
10747
  // Explicit Cloud may provision or wake the bot's own Boat at dispatch.
10521
10748
  // This probe only checks its identity/account, without starting billing.
@@ -11208,7 +11435,7 @@ const webhooks = new WebhookManager({
11208
11435
  post: (botId, threadId, text) => {
11209
11436
  if (!store.bot(botId))
11210
11437
  return;
11211
- store.appendMessage(threadId, { role: "bot", kind: "text", text });
11438
+ store.appendMessage(threadId, { role: "bot", kind: "text", text, webhookPost: true });
11212
11439
  },
11213
11440
  // Mirrors resolveResultsThread's routines wiring a few hundred lines up
11214
11441
  // in this same file: create-on-first-use, never activated (so it never
@@ -11446,6 +11673,10 @@ setupRetry = 0) {
11446
11673
  noteForeignTurn(bot.id, threadId, internalGeneration);
11447
11674
  const resourceOwner = { threadId, generation: internalGeneration };
11448
11675
  turnResourceOwners.set(threadId, resourceOwner);
11676
+ // Set when this member's cloud computer, claimed by its first computer
11677
+ // call, could not start (or parked waiting for its seat): the turn ends on
11678
+ // it, with this cause, instead of as merely interrupted.
11679
+ let roomClaimFailure;
11449
11680
  let roomVmTarget = null;
11450
11681
  let retainRoomVmLease = false;
11451
11682
  let roomSpeaker;
@@ -11692,17 +11923,13 @@ setupRetry = 0) {
11692
11923
  const roomPlaceRefusal = roomKind && computerPlaceRefusal(roomKind);
11693
11924
  if (roomPlaceRefusal)
11694
11925
  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
11926
  // The same one rule as a bot thread, before anything is provisioned: an
11701
11927
  // engine that cannot use the cloud computer is refused, never swapped.
11702
11928
  if (!roomTeamComputer && roomPlan.computer === "cloud") {
11703
- const unsupported = cloudPlaceDriverError(cloudEngine(instance), turnProvider(readyBot) === "vps" ? "vps" : "box");
11929
+ const unsupported = cloudPlaceRefusal(cloudEngine(instance, readyBot.modelSelection.model), "room", readyBot.name)
11930
+ ?? computerToolsRefusal(readyBot.toolScope, "room", readyBot.name);
11704
11931
  if (unsupported)
11705
- throw new PlaceUnavailableError("cloud", unsupported);
11932
+ throw unsupported;
11706
11933
  }
11707
11934
  // One place per room turn as well: a team computer reached on Auto means
11708
11935
  // no separate built-in browser.
@@ -11722,12 +11949,12 @@ setupRetry = 0) {
11722
11949
  return false;
11723
11950
  }
11724
11951
  if (roomTeamComputer) {
11725
- const attached = await attachTeamBoat(roomTeamComputer, readyBot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance), "box"), instance.adapter.capabilities.remoteAgent === true);
11952
+ const attached = await attachTeamBoat(roomTeamComputer, readyBot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance)));
11726
11953
  if (isCancelled?.() || groupSpeakers.get(threadId) !== roomSpeaker ||
11727
11954
  activeInternalGenerationByThread.get(threadId) !== internalGeneration)
11728
11955
  return false;
11729
11956
  Object.assign(integrations, attached.mount);
11730
- startScreenPoller(readyBot.id, threadId, { computer: attached.capture }, { screenIsTheWork: instance.adapter.capabilities.remoteAgent === true });
11957
+ startScreenPoller(readyBot.id, threadId, { computer: attached.capture });
11731
11958
  }
11732
11959
  // The speaker's own explicit place, mounted exactly as its bot thread
11733
11960
  // mounts it. A channel has no conversation pin and no Auto fallback here:
@@ -11735,6 +11962,51 @@ setupRetry = 0) {
11735
11962
  const roomSetupIsCurrent = () => !isCancelled?.() &&
11736
11963
  groupSpeakers.get(threadId) === roomSpeaker &&
11737
11964
  activeInternalGenerationByThread.get(threadId) === internalGeneration;
11965
+ // A lazily claimed desktop (the VPS, the member's Boat) shows in the room
11966
+ // from the moment its first computer call claims it.
11967
+ const showClaimedScreen = (capture) => {
11968
+ if (roomSetupIsCurrent() && store.group(readyGroup.id)?.busyBotId === readyBot.id) {
11969
+ roomScreenBotId = readyBot.id;
11970
+ startScreenPoller(readyBot.id, threadId, { computer: capture });
11971
+ }
11972
+ };
11973
+ // The member's cloud computer could not start on its first computer call:
11974
+ // the same row, report and parking a failure at setup gets (the catch at
11975
+ // the end of this function), then the member's turn ends. Fenced to this
11976
+ // member's still-running turn.
11977
+ const failRoomClaim = (failure, error) => {
11978
+ if (activeInternalGenerationByThread.get(threadId) !== internalGeneration || groupSpeakers.get(threadId) !== roomSpeaker)
11979
+ return;
11980
+ if (resourceOwner.computerParkedOn) {
11981
+ registerComputerResume({
11982
+ botId: bot.id,
11983
+ threadId,
11984
+ resource: resourceOwner.computerParkedOn,
11985
+ generation: internalGeneration,
11986
+ afterMessageId: store.activePath(threadId).findLast((entry) => entry.role === "user")?.id,
11987
+ });
11988
+ roomClaimFailure = { parked: true };
11989
+ if (orchestration)
11990
+ orchestration.result.stopReason = `${bot.name} parked waiting for the computer`;
11991
+ }
11992
+ else {
11993
+ // Read as a room member's place (mountBotBoat): its one line, with no
11994
+ // retry of its own.
11995
+ const placeFailure = error instanceof PlaceUnavailableError ? error : undefined;
11996
+ const message = placeFailure?.message ?? failure;
11997
+ roomClaimFailure = { parked: false, message };
11998
+ store.appendMessage(threadId, {
11999
+ role: "bot", kind: "activity",
12000
+ from: { botId: bot.id, name: bot.name, color: bot.color },
12001
+ tool: failedTurnTool(message, placeFailure ? { place: placeFailure.row } : {}),
12002
+ });
12003
+ onDispatchError?.(message);
12004
+ // Claude settles the interrupt as exit_before_result: not a second
12005
+ // incident for the same failure.
12006
+ resourceOwner.lazyClaimFailureReported = true;
12007
+ }
12008
+ void instance.adapter.interruptTurn(threadId).catch(() => { });
12009
+ };
11738
12010
  // An explicit place that cannot be attached fails the member's turn with
11739
12011
  // its cause and the one setting that changes it.
11740
12012
  try {
@@ -11748,15 +12020,7 @@ setupRetry = 0) {
11748
12020
  if (turnProvider(readyBot) === "vps") {
11749
12021
  vpsThreadStarted(readyBot.id, threadId);
11750
12022
  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
- });
12023
+ const mounted = await mountBotVps(readyBot, resourceOwner, { start: true, onClaimed: showClaimedScreen });
11760
12024
  if (!roomSetupIsCurrent())
11761
12025
  return false;
11762
12026
  if (!("integration" in mounted))
@@ -11766,24 +12030,18 @@ setupRetry = 0) {
11766
12030
  }
11767
12031
  else {
11768
12032
  if (!boat.boatConfigured(cfg))
11769
- throw new Error(BOAT_NOT_CONFIGURED);
11770
- const remoteAgent = instance.adapter.capabilities.remoteAgent === true;
11771
- const attached = await attachBotBoat(readyBot, resourceOwner, { explicitCloud: true, remoteAgent });
11772
- if (!roomSetupIsCurrent())
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 });
12033
+ throw boatNotConfigured(readyBot, "room");
12034
+ // As in a bot thread: created or woken by the member's first
12035
+ // computer call, never by the room message itself.
12036
+ integrations.localComputer = mountBotBoat(readyBot, resourceOwner, { source: "room", onClaimed: showClaimedScreen, onRejected: failRoomClaim });
11779
12037
  roomComputerKind = "box";
11780
12038
  }
11781
12039
  }
11782
12040
  // Room and Goal turns use the speaker's desktop, never the coordinator's.
11783
12041
  // Claim the same lease as direct turns before asynchronous VM setup.
11784
12042
  if (readyBot.computer === "vm") {
11785
- if (instance.adapter.capabilities.computerMcp !== true || instance.adapter.capabilities.remoteAgent === true) {
11786
- throw new Error("this model cannot use the Local VM");
12043
+ if (instance.adapter.capabilities.computerMcp !== true) {
12044
+ throw placeUnavailable("vm", { state: "vm-cannot", params: { bot: readyBot.name, model: cloudEngine(instance, readyBot.modelSelection.model).name }, source: "room" }, Boolean(CLOUD_HOME));
11787
12045
  }
11788
12046
  // A distinct identity fences cleanup even in shared mode on the same room thread.
11789
12047
  const target = { ...localVmTargetForThread(readyBot.id, threadId) };
@@ -11823,8 +12081,7 @@ setupRetry = 0) {
11823
12081
  const place = roomPlan.computer;
11824
12082
  if (place !== "cloud" && place !== "vm" && place !== "local")
11825
12083
  throw error;
11826
- // A room row is not cut, so the cause keeps all its words.
11827
- throw asPlaceFailure(error, place, "works-on", Infinity);
12084
+ throw await asPlaceFailure(error, place, "room", readyBot, turnProvider(readyBot) === "vps" ? "vps" : "box");
11828
12085
  }
11829
12086
  const roster = readyGroup.memberIds
11830
12087
  .map((id) => store.bot(id))
@@ -11911,7 +12168,6 @@ setupRetry = 0) {
11911
12168
  const recentLines = recentWork(recentWorkSources(bot), bot, { userName, currentThreadId: threadId, ...recentWorkFilter() });
11912
12169
  const roomComputerPromptKind = resolveComputerPromptKind({
11913
12170
  kind: roomTeamComputer || roomComputerKind === "box" ? "box" : roomVmTarget ? "vm" : roomComputerKind,
11914
- driverKind: instance.driverKind,
11915
12171
  vmPrivate: localVmMode(cfg) === "per-bot",
11916
12172
  });
11917
12173
  {
@@ -11932,7 +12188,7 @@ setupRetry = 0) {
11932
12188
  { id: "computer", label: "Computer", text: computerPrompt(roomComputerPromptKind) },
11933
12189
  { id: "team-computer", label: "Team computer", text: teamComputerPrompt(roomTeamComputer) },
11934
12190
  { 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: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
12191
+ { id: "cloud-home", label: "My Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
11936
12192
  { id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
11937
12193
  { id: "recall", label: "Recall", text: integrations.agents && bot.memoryEnabled !== false ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
11938
12194
  teamAvailabilityPart(outsideRoom),
@@ -12025,13 +12281,10 @@ setupRetry = 0) {
12025
12281
  if (e.type === "item.completed" && e.itemType === "assistant_text")
12026
12282
  replyText += `\n${e.text}`;
12027
12283
  else if (e.type === "turn.completed") {
12028
- if (orchestration && !e.ok) {
12029
- orchestration.result.stopReason = e.stopReason ?? null;
12030
- finish("provider_failed");
12031
- }
12032
- else {
12033
- finish("settled");
12034
- }
12284
+ const end = roomTurnEnd(e, Boolean(orchestration), roomClaimFailure);
12285
+ if (orchestration && end.stopReason !== undefined)
12286
+ orchestration.result.stopReason = end.stopReason;
12287
+ finish(end.outcome);
12035
12288
  }
12036
12289
  // Waiting on a person is not turn work: hold the ceiling while an
12037
12290
  // approval or question card is open, so deciding slowly does not
@@ -12220,7 +12473,7 @@ setupRetry = 0) {
12220
12473
  drainSecretResumes();
12221
12474
  drainTeamSetupResumes();
12222
12475
  }
12223
- if (outcome === "provider_failed") {
12476
+ if (outcome === "provider_failed" || outcome === "parked") {
12224
12477
  if (skillAuthoring)
12225
12478
  skillAuthoringClaim.claimed = false;
12226
12479
  return false;
@@ -12309,7 +12562,7 @@ setupRetry = 0) {
12309
12562
  store.appendMessage(threadId, {
12310
12563
  role: "bot", kind: "activity",
12311
12564
  from: { botId: bot.id, name: bot.name, color: bot.color },
12312
- tool: failedTurnTool(message),
12565
+ tool: failedTurnTool(message, error instanceof PlaceUnavailableError ? { place: error.row } : {}),
12313
12566
  });
12314
12567
  onDispatchError?.(message);
12315
12568
  if (orchestration) {
@@ -14124,6 +14377,10 @@ function persistMcpServers(next) {
14124
14377
  }
14125
14378
  async function describeInstances() {
14126
14379
  const configs = providerConfigs();
14380
+ // Reading the engines is where this server learns one became available
14381
+ // (an install, a sign-in, a key or a Company engine), so a bot still on the
14382
+ // removed Computer engine moves now rather than at the next start.
14383
+ void retryComputerEngineMove();
14127
14384
  return (await registry.describe()).map((instance) => {
14128
14385
  const entry = configs[instance.instanceId];
14129
14386
  const described = {
@@ -14552,7 +14809,7 @@ const liveCalls = new LiveCallController({
14552
14809
  },
14553
14810
  broadcast: (frame) => broadcast(frame, { adminOnly: true }),
14554
14811
  settings: () => ({ key: cfg.live?.key ?? "", ...liveSettingsFor(cfg) }),
14555
- createSession: ({ key, sdp, botId, threadId, voice }) => createLiveSession({ key, sdp, voice, bot: liveBotFor(botId), history: liveHistoryFor(threadId) }),
14812
+ createSession: ({ key, sdp, botId, threadId, voice }) => createLiveSession({ key, sdp, voice, bot: liveBotFor(botId), history: liveHistoryFor(threadId), cloudHome: Boolean(CLOUD_HOME) }),
14556
14813
  // Node's WebSocket (undici) accepts headers in its second argument.
14557
14814
  openSocket: (url, key) => new WebSocket(url, { headers: { authorization: `Bearer ${key}` } }),
14558
14815
  attachUrl: (sessionId) => liveAttachUrl(sessionId),
@@ -15047,7 +15304,7 @@ const handleRequest = async (req, res) => {
15047
15304
  // A Cloud home is one person's: only their own devices, which the
15048
15305
  // Admin's pairing signs in with admin scope, may lend to it.
15049
15306
  if (CLOUD_HOME && !auth.scopes.includes("admin"))
15050
- return json(res, 403, { error: "Only your own computers can lend to your Cloud. Connect this computer to your Cloud from its OMB Cloud settings." });
15307
+ 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
15308
  sharedComputers.register(registration, { session: auth.session.id, person: CLOUD_HOME ? CLOUD_HOME_LENDER : personKey(auth.session) }, secret);
15052
15309
  return json(res, 200, { ok: true });
15053
15310
  }
@@ -15230,9 +15487,13 @@ const handleRequest = async (req, res) => {
15230
15487
  return json(res, 409, { error: "The user request ended before its computer was selected." });
15231
15488
  if (unoffered)
15232
15489
  return json(res, 409, { error: unoffered, options });
15233
- const option = requested === "auto"
15234
- ? options.find(option => option.ready && option.surface === current) ?? options.find(option => option.ready && option.surface === "vm") ?? options.find(option => option.ready) ?? options.find(option => option.canStart) ?? options.find(option => option.canCreate && option.surface === "vm") ?? options.find(option => option.canCreate)
15235
- : options.find(option => option.surface === requested);
15490
+ // The computer this turn mounted is already selected, even before it
15491
+ // is up when its first call starts it (the bot's cloud computer):
15492
+ // switching to it would only restart the request.
15493
+ const claim = autoVmClaims.get(internalCapability.threadId);
15494
+ const startsOnFirstCall = claim?.owner.generation === internalCapability.generation && claim.startsComputer === true && !claim.failed;
15495
+ const picked = pickComputer(options, requested, current, startsOnFirstCall);
15496
+ const option = picked.option;
15236
15497
  if (!option?.available)
15237
15498
  return json(res, 409, { error: option?.reason ?? "No configured computer or browser is available. Open the Computer panel to set one up.", options });
15238
15499
  if (source.selected) {
@@ -15240,8 +15501,11 @@ const handleRequest = async (req, res) => {
15240
15501
  return json(res, 409, { error: "A computer switch is already pending. End this turn to continue there." });
15241
15502
  }
15242
15503
  else {
15243
- if (option.surface === current && option.ready)
15244
- return json(res, 200, { status: "ready", surface: current, message: "This computer is already selected. Use its mounted tools." });
15504
+ if (picked.current) {
15505
+ return json(res, 200, { status: "ready", surface: current, message: option.ready
15506
+ ? "This computer is already selected. Use its mounted tools."
15507
+ : "This computer is already selected. It starts on its first call; use its mounted tools." });
15508
+ }
15245
15509
  source.selected = option.surface;
15246
15510
  source.previousSurface = store.taskByThread(bot.id, bot.threadId)?.surface;
15247
15511
  }
@@ -15343,17 +15607,19 @@ const handleRequest = async (req, res) => {
15343
15607
  return json(res, 200, { result });
15344
15608
  }
15345
15609
  // The cloud computer's tools (harness-mcp-proxy computer): the turn's
15346
- // capability names its Boat, and the Boat credential stays here.
15610
+ // capability names its Boat, and the Boat credential stays here. A
15611
+ // capability minted before the Boat existed (boxId null) acts on the
15612
+ // one its first call's claim created or woke (mountBotBoat).
15347
15613
  if (method === "POST" && path === "/api/internal/computer/mcp") {
15348
15614
  const body = await readInternalBody();
15349
- const boxId = internalCapability.boxId;
15350
- if (!boxId)
15615
+ if (internalCapability.boxId === undefined)
15351
15616
  return json(res, 403, { error: "this turn has no cloud computer" });
15352
15617
  const abort = new AbortController();
15353
15618
  res.once("close", () => { if (!res.writableEnded)
15354
15619
  abort.abort(); });
15355
15620
  return json(res, 200, { result: await cloudComputerRpc(body, {
15356
- cfg, boxId, signal: abort.signal,
15621
+ cfg, signal: abort.signal,
15622
+ boxId: () => internalCapability.boxId ?? claimedBoatId(internalCapability),
15357
15623
  gate: () => computerCallGate(internalCapability),
15358
15624
  assertActive: requireActiveInternalCapability,
15359
15625
  }) });
@@ -16194,7 +16460,8 @@ const handleRequest = async (req, res) => {
16194
16460
  const guestThread = guestWorkThread(from, target, fromThreadId, message);
16195
16461
  if (guestThread === null)
16196
16462
  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 } : {}) }, MAX_COMMS_DEPTH, fromThreadId);
16463
+ const queued = queueDelegation(commsBus, from, { toBotId, message, reason: "asked while busy", depth, approvalAlreadyGranted, ...(guestThread ? { targetThreadId: guestThread } : {}),
16464
+ ...externalCompletion(internalCapability) }, MAX_COMMS_DEPTH, fromThreadId);
16198
16465
  if ((queued.result !== "ok" || !queued.id) && guestThread)
16199
16466
  store.deleteTask(toBotId, guestThread);
16200
16467
  if (queued.result !== "ok" || !queued.id)
@@ -16362,6 +16629,7 @@ const handleRequest = async (req, res) => {
16362
16629
  sourceThreadId: fromThreadId,
16363
16630
  sourceBotId: currentFrom.id,
16364
16631
  routineRunId: activeRoutineRunForThread(fromThreadId)?.id,
16632
+ ...externalCompletion(internalCapability),
16365
16633
  });
16366
16634
  store.appendMessage(fromThreadId, {
16367
16635
  role: "bot",
@@ -16566,7 +16834,7 @@ const handleRequest = async (req, res) => {
16566
16834
  const guestThread = guestWorkThread(from, target, fromThreadId, reason ?? message);
16567
16835
  if (guestThread === null)
16568
16836
  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);
16837
+ const queued = queueDelegation(commsBus, from, { toBotId, message, reason, depth, ...(guestThread ? { targetThreadId: guestThread } : {}), ...externalCompletion(internalCapability) }, MAX_COMMS_DEPTH, fromThreadId);
16570
16838
  if ((queued.result !== "ok" || !queued.id) && guestThread)
16571
16839
  store.deleteTask(toBotId, guestThread);
16572
16840
  if (queued.result !== "ok" || !queued.id) {
@@ -17546,8 +17814,15 @@ const handleRequest = async (req, res) => {
17546
17814
  const policy = currentSender.outbound ?? DEFAULT_OUTBOUND_POLICY;
17547
17815
  const threadId = internalCapability.threadId;
17548
17816
  const { app } = describeTool(outboundTool);
17549
- const summary = outboundCalls.map((call) => {
17817
+ // One entry per call, in the summary's order, so a phone can say
17818
+ // "Send to Linear? Create linear comment ×2" without parsing the
17819
+ // raw arguments out of the subtitle.
17820
+ const calls = outboundCalls.map((call) => {
17550
17821
  const described = describeTool(call.slug);
17822
+ return { app: described.app, label: described.label };
17823
+ });
17824
+ const summary = outboundCalls.map((call, index) => {
17825
+ const described = calls[index];
17551
17826
  const argsText = call.arguments === undefined ? "" : JSON.stringify(call.arguments);
17552
17827
  return `${described.app ? `${described.app} · ` : ""}${described.label}${argsText ? `\n${argsText.slice(0, 400)}${argsText.length > 400 ? "… [arguments truncated]" : ""}` : ""}`;
17553
17828
  }).join("\n\n");
@@ -17577,7 +17852,7 @@ const handleRequest = async (req, res) => {
17577
17852
  tool: outboundTool,
17578
17853
  held: HELD_NOTE["approval.held.outbound"],
17579
17854
  heldCode: "approval.held.outbound",
17580
- outboundRequest: { tool: outboundTool, app },
17855
+ outboundRequest: { tool: outboundTool, app, calls },
17581
17856
  },
17582
17857
  });
17583
17858
  appendDecision(DATA_DIR, {
@@ -17672,7 +17947,8 @@ const handleRequest = async (req, res) => {
17672
17947
  if (upstream.transportSessionId)
17673
17948
  headers["mcp-session-id"] = upstream.transportSessionId;
17674
17949
  res.writeHead(upstream.status, headers);
17675
- return res.end(Buffer.from(upstream.bytes));
17950
+ // A missing OAuth permission gets a note on what fixes it (MOCA-273).
17951
+ return res.end(Buffer.from(withScopeHint(upstream.bytes, upstream.contentType, connectorCalls.map((call) => call.slug))));
17676
17952
  }
17677
17953
  // ── computer control: proxies read the hold, bots plead for help ──
17678
17954
  if (path === "/api/internal/computer-control") {
@@ -22252,8 +22528,6 @@ const handleRequest = async (req, res) => {
22252
22528
  const key = teamComputerOwner(found.id);
22253
22529
  if (parsed.data.action === "take" && boatLifecycleBusyBots.has(key))
22254
22530
  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
22531
  if (parsed.data.controlLeaseId) {
22258
22532
  const result = parsed.data.action === "take"
22259
22533
  ? computerControl.acquireLease(key, parsed.data.controlLeaseId)
@@ -22292,7 +22566,7 @@ const handleRequest = async (req, res) => {
22292
22566
  if (method === "POST" && !computerId) {
22293
22567
  const parsed = teamComputerCreate.safeParse(body);
22294
22568
  if (!parsed.success)
22295
- return json(res, 400, { error: "Provide requestId (UUID), a name, and acknowledgeCost: true to create a paid Boat" });
22569
+ return json(res, 400, { error: "Provide requestId (UUID), a name, and acknowledgeCost: true to create a paid cloud computer" });
22296
22570
  if (!boat.boatConfigured(cfg))
22297
22571
  return json(res, 409, { error: "Configure Boat in Settings before creating a cloud computer" });
22298
22572
  const computer = teamComputers.create(parsed.data.name, parsed.data.requestId);
@@ -22312,7 +22586,7 @@ const handleRequest = async (req, res) => {
22312
22586
  }
22313
22587
  if (method === "POST" && found && action && action !== "control") {
22314
22588
  if (action === "provision" && body?.acknowledgeCost !== true)
22315
- return json(res, 400, { error: "Confirm Boat creation or wake costs with acknowledgeCost: true" });
22589
+ return json(res, 400, { error: "Confirm cloud computer creation or wake costs with acknowledgeCost: true" });
22316
22590
  // A ready-only join never wakes, provisions or steals an agent's turn.
22317
22591
  // The caller takes a separate explicit human-control lease first.
22318
22592
  if (action === "join") {
@@ -22634,7 +22908,12 @@ const handleRequest = async (req, res) => {
22634
22908
  // The bots' browser engine: install it on this machine (agent-browser +
22635
22909
  // a Chrome for Testing, a one-time download), or ask how that is going.
22636
22910
  // One install at a time; the config frame's browserEngine tells the rest.
22911
+ // JSON only, so a form posted from another local page cannot start it.
22637
22912
  if (method === "POST" && path === "/api/browser-engine/install") {
22913
+ const mediaType = String(req.headers["content-type"] ?? "").split(";", 1)[0].trim().toLowerCase();
22914
+ if (mediaType !== "application/json") {
22915
+ return json(res, 415, { error: "content-type must be application/json" });
22916
+ }
22638
22917
  if (!browserEngineInstall) {
22639
22918
  const status = browserEngineStatus();
22640
22919
  if (status.kind === "unavailable" && !status.installable)
@@ -24285,9 +24564,6 @@ const handleRequest = async (req, res) => {
24285
24564
  if (!currentBot)
24286
24565
  return json(res, 404, { error: "no such bot" });
24287
24566
  const controlKey = botComputerControlKey(currentBot);
24288
- const teamComputer = inheritedTeamComputer(currentBot);
24289
- if (action === "take" && teamComputer)
24290
- assertTeamControlCanBeTaken(teamComputer.id);
24291
24567
  const leaseResult = body.controlLeaseId === undefined
24292
24568
  ? null
24293
24569
  : controlLeaseIdSchema.safeParse(body.controlLeaseId);
@@ -24464,12 +24740,12 @@ const handleRequest = async (req, res) => {
24464
24740
  }
24465
24741
  if (bot.computer !== "cloud" && !threadPreview) {
24466
24742
  return json(res, 409, {
24467
- error: "Choose Cloud before changing or opening this Boat. Auto only checks existing computer state.",
24743
+ error: "Choose Cloud computer before changing or opening this cloud computer. Auto only checks existing computer state.",
24468
24744
  });
24469
24745
  }
24470
24746
  if (m[2] === "remove") {
24471
24747
  // Boats sleep and wake; only the VPS backend has a container to remove.
24472
- return json(res, 409, { error: "the cloud Boat backend has no container to remove — use sleep instead" });
24748
+ return json(res, 409, { error: "a cloud computer has no container to remove — put it to sleep instead" });
24473
24749
  }
24474
24750
  const releaseComputerLifecycle = claimBotComputerLifecycle(botId);
24475
24751
  try {