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.
- package/dist/assets/{DesktopViewer-DsnA23KM.js → DesktopViewer-CPidMCtt.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
- package/dist/assets/{arc-Duj5Coy2.js → arc-BUWGa9UU.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-uYUsDawP.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
- package/dist/assets/channel-DR8eD6QW.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D-Uglxqq.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-CKV5D2F6.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
- package/dist/assets/{index-DreDnjOu.js → index-Cp4S4NOL.js} +1 -1
- package/dist/assets/index-CpaIXRLp.js +614 -0
- package/dist/assets/index-Z81SMsgR.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
- package/dist/assets/{linear-Bl669148.js → linear-BASsfWly.js} +1 -1
- package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-BVidiPkk.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -1
- package/dist-server/drivers/agents-proxy.js +5 -5
- package/dist-server/index.js +2379 -1596
- package/dist-server/openmausbot.js +857 -1012
- package/dist-server/pair-cli.js +857 -1012
- package/dist-server/permission-proxy.js +0 -4
- package/dist-server/server/auto-approve.js +1 -1
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +68 -84
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +275 -44
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +1 -1
- package/dist-server/server/cloud-home.js +3 -9
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +7 -11
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/default-model-selection.js +20 -14
- package/dist-server/server/delegations.js +11 -5
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +1 -1
- package/dist-server/server/drivers/acp/opencode-go.js +16 -21
- package/dist-server/server/drivers/agents-call.js +1 -1
- package/dist-server/server/drivers/agents-catalog.js +3 -3
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/claude.js +49 -30
- package/dist-server/server/drivers/codex.js +2 -2
- package/dist-server/server/drivers/openai-chat.js +98 -6
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/index.js +555 -279
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-roster.js +10 -2
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/store.js +125 -1
- package/dist-server/server/surface.js +29 -49
- package/dist-server/server/system-prompt.js +10 -10
- package/dist-server/server/turn-continuation.js +162 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/ask-question.js +0 -70
- package/dist-server/shared/cloud-computer.js +4 -6
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/failed-turn.js +1 -4
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/package.json +1 -1
- package/dist/assets/channel-BZxwC-Gw.js +0 -1
- package/dist/assets/index-Du7o6Dsf.js +0 -610
- package/dist/assets/index-KxG4bWSr.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
- package/dist-server/server/drivers/boatagent.js +0 -443
|
@@ -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,
|
|
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 {
|
|
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 {
|
|
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(
|
|
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
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
function
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
function
|
|
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
|
-
|
|
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;
|
|
2771
|
-
// While enrolled, a Company model that can run beats
|
|
2772
|
-
// engine, and the organisation's policy is respected;
|
|
2773
|
-
|
|
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(
|
|
2776
|
-
return selectDefaultModelSelection(await registry.describe(),
|
|
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: "
|
|
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
|
|
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
|
-
|
|
5960
|
-
|
|
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.
|
|
5966
|
-
//
|
|
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,
|
|
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
|
|
6195
|
+
async function attachTeamBoat(computer, botId, owner, canMount) {
|
|
6064
6196
|
if (!canMount)
|
|
6065
|
-
throw new Error("This model cannot use the team's
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
6221
|
+
mount: cloudComputerMount(botId, owner, machine.id),
|
|
6090
6222
|
capture: () => boat.screenshotBoat(cfg, ownerId, machine.id),
|
|
6091
6223
|
};
|
|
6092
6224
|
}
|
|
6093
|
-
/** The
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
*
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
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
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
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.
|
|
6141
|
-
*
|
|
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
|
-
/**
|
|
6173
|
-
*
|
|
6174
|
-
*
|
|
6175
|
-
*
|
|
6176
|
-
*
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
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(
|
|
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(
|
|
6417
|
+
lifecycle = lifecycleOf();
|
|
6211
6418
|
}
|
|
6212
|
-
|
|
6213
|
-
return null;
|
|
6214
|
-
const machine = b;
|
|
6215
|
-
await bindTurnComputer(owner, `computer:box:${machine.id}`, opts.remoteAgent);
|
|
6216
|
-
return {
|
|
6217
|
-
capture: () => boat.screenshotBoat(cfg, bot.id, machine.id),
|
|
6218
|
-
mount: cloudComputerMount(bot.id, owner, machine.id, opts.remoteAgent),
|
|
6219
|
-
};
|
|
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
|
-
|
|
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)
|
|
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)
|
|
6562
|
+
else if (boat.boatConfigured(cfg) && canWorkOnCloud(cloudEngine(instance))) {
|
|
6364
6563
|
const status = await boat.boatStatus(cfg, bot.id);
|
|
6365
|
-
const lifecycle = boat.boatTurnLifecycleAction(
|
|
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" &&
|
|
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
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
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
|
|
8473
|
-
*
|
|
8474
|
-
*
|
|
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
|
|
8568
|
-
*
|
|
8569
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
9158
|
-
//
|
|
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 =
|
|
9372
|
+
const unsupported = cloudPlaceRefusal(cloudEngine(instance, bot.modelSelection.model), placeSource, bot.name)
|
|
9373
|
+
?? computerToolsRefusal(toolScope, placeSource, bot.name);
|
|
9161
9374
|
if (unsupported)
|
|
9162
|
-
throw
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
9687
|
-
//
|
|
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: "
|
|
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 } : {}) }
|
|
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
|
|
9954
|
-
//
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
if (e instanceof PlaceUnavailableError) {
|
|
9958
|
-
const pinned = store.taskByThread(bot.id, threadId);
|
|
9959
|
-
if (pinned?.surface === e.place && pinned.surfaceSource === "auto") {
|
|
9960
|
-
store.patchTask(bot.id, threadId, { surface: undefined, surfaceSource: undefined });
|
|
9961
|
-
}
|
|
9962
|
-
}
|
|
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,
|
|
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
|
-
|
|
10284
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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)
|
|
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 }
|
|
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
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
return false;
|
|
11774
|
-
if (!attached)
|
|
11775
|
-
throw new Error("the cloud computer could not be created or reached");
|
|
11776
|
-
Object.assign(integrations, attached.mount);
|
|
11777
|
-
roomScreenBotId = readyBot.id;
|
|
11778
|
-
startScreenPoller(readyBot.id, threadId, { computer: attached.capture }, { screenIsTheWork: remoteAgent });
|
|
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
|
|
11786
|
-
throw
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
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
|
|
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
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
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 (
|
|
15244
|
-
return json(res, 200, { status: "ready", surface: current, message:
|
|
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
|
-
|
|
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,
|
|
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 } : {})
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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 {
|