openmausbot 0.1.95 → 0.1.96
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-BwWpc4RK.js → DesktopViewer-DsnA23KM.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
- package/dist/assets/{arc-7kei4Hsu.js → arc-Duj5Coy2.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
- package/dist/assets/channel-BZxwC-Gw.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
- package/dist/assets/{index-KygHRE3E.js → index-DreDnjOu.js} +1 -1
- package/dist/assets/index-Du7o6Dsf.js +610 -0
- package/dist/assets/index-KxG4bWSr.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
- package/dist/assets/{linear-CcR4pc0-.js → linear-Bl669148.js} +1 -1
- package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-86Xqz5Mr.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +25 -12
- package/dist-server/companion/src/listener.js +39 -18
- package/dist-server/container-mcp.js +14 -27
- package/dist-server/drivers/agents-proxy.js +103 -44
- package/dist-server/harness-mcp-proxy.js +1 -2
- package/dist-server/index.js +2633 -3396
- package/dist-server/openmausbot.js +606 -463
- package/dist-server/pair-cli.js +606 -463
- package/dist-server/server/admin-activity.js +1 -4
- package/dist-server/server/auto-approve.js +19 -4
- package/dist-server/server/boat.js +4 -4
- package/dist-server/server/checkpoints.js +90 -148
- package/dist-server/server/chief-of-staff.js +7 -6
- package/dist-server/server/cli.js +33 -26
- package/dist-server/server/cloud-home-start.js +12 -15
- package/dist-server/server/cloud-home.js +0 -4
- package/dist-server/server/cloud-move-http.js +44 -30
- package/dist-server/server/cloud-move.js +20 -18
- package/dist-server/server/composio.js +10 -3
- package/dist-server/server/config.js +92 -64
- package/dist-server/server/container-computer.js +16 -5
- package/dist-server/server/contracts.js +0 -9
- package/dist-server/server/decider/jev.js +1 -4
- package/dist-server/server/decider/log.js +1 -3
- package/dist-server/server/decision-log.js +1 -4
- package/dist-server/server/delegations.js +3 -2
- package/dist-server/server/drivers/acp/core.js +4 -3
- package/dist-server/server/drivers/acp/opencode-go.js +17 -16
- package/dist-server/server/drivers/agents-call.js +46 -63
- package/dist-server/server/drivers/agents-catalog.js +66 -0
- package/dist-server/server/drivers/agents-proxy.js +2 -1
- package/dist-server/server/drivers/claude-login-auth.js +2 -11
- package/dist-server/server/drivers/codex-device-auth.js +3 -1
- package/dist-server/server/drivers/codex.js +5 -4
- package/dist-server/server/drivers/grok.js +1 -1
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
- package/dist-server/server/drivers/openai-chat.js +15 -5
- package/dist-server/server/drivers/prompt-split.js +4 -4
- package/dist-server/server/harness/bus.js +49 -0
- package/dist-server/server/harness-mcp-proxy.js +1 -1
- package/dist-server/server/index.js +448 -762
- package/dist-server/server/mcp-registry.js +35 -0
- package/dist-server/server/message-file.js +4 -4
- package/dist-server/server/peer-delivery.js +4 -19
- package/dist-server/server/peer-roster.js +30 -4
- package/dist-server/server/profile-versions.js +0 -31
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/restart.js +40 -0
- package/dist-server/server/room-handoffs.js +12 -0
- package/dist-server/server/routes/bot-memory.js +0 -24
- package/dist-server/server/routine-requests.js +1 -4
- package/dist-server/server/routines.js +5 -4
- package/dist-server/server/server-launcher.js +53 -0
- package/dist-server/server/store.js +1 -1
- package/dist-server/server/surface.js +23 -27
- package/dist-server/server/system-prompt.js +13 -3
- package/dist-server/server/team-manifest.js +1 -6
- package/dist-server/server/tunnel.js +4 -1
- package/dist-server/server/turn-images.js +1 -21
- package/dist-server/server/turn-resources.js +19 -150
- package/dist-server/server/usage-ledger.js +3 -1
- package/dist-server/server/workspace.js +2 -3
- package/dist-server/server-launcher.js +72 -0
- package/dist-server/shared/ask-question.js +1 -1
- package/dist-server/shared/cloud-computer.js +12 -0
- package/dist-server/shared/failed-turn.js +25 -0
- package/dist-server/shared/live-approval.js +2 -2
- package/dist-server/shared/package-format.js +3 -3
- package/dist-server/shared/pairing-link.js +144 -0
- package/dist-server/vps-container-mcp.js +14 -27
- package/dist-server/workspace-backup.worker.js +14 -27
- package/package.json +1 -1
- package/dist/assets/channel-DS2NbT5x.js +0 -1
- package/dist/assets/index-NF7XCHAy.css +0 -1
- package/dist/assets/index-X_5ydXIE.js +0 -610
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
- package/dist-server/server/claim-idle.js +0 -33
- package/dist-server/server/cloud-overflow.js +0 -167
- package/dist-server/server/tightening-requests.js +0 -435
- package/dist-server/shared/tightening-request.js +0 -87
|
@@ -27,6 +27,9 @@ import { assertRequestTarget, guardedRequestPath, requestConflict, requestNeedsI
|
|
|
27
27
|
import { botAvatarUrlFromStoredPath } from "../shared/bot-avatar.js";
|
|
28
28
|
import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.js";
|
|
29
29
|
import { CLOUD_COMPUTER_BUSY_ERROR } from "../shared/computer-contention.js";
|
|
30
|
+
import { failedTurnTool } from "../shared/failed-turn.js";
|
|
31
|
+
import { phonePairingLink } from "../shared/pairing-link.js";
|
|
32
|
+
import { canWorkOnCloud } from "../shared/cloud-computer.js";
|
|
30
33
|
import { approvalModeFor, modelSwitchNeedsAsk, isEmergencyApprovalDowngrade, isApprovalMode, } from "../shared/approval-mode.js";
|
|
31
34
|
import { createApprovalModeSupport } from "./harness-capabilities.js";
|
|
32
35
|
import { escapeAttribute } from "../src/lib/composer-attachments.js";
|
|
@@ -58,7 +61,7 @@ import { isPersistentQuestionCard, QUESTION_DISMISS_MESSAGE, shouldSettleRequest
|
|
|
58
61
|
import { parseToolScope, toolScopeWidens } from "../shared/tool-scope.js";
|
|
59
62
|
import { boatCreateRecoverySnapshot, retireDeletedBoatCreate } from "./boat-create-idempotency.js";
|
|
60
63
|
import { boatDeletionSnapshot } from "./boat-delete-journal.js";
|
|
61
|
-
import { liveDecisionRefusal } from "../shared/live-approval.js";
|
|
64
|
+
import { liveCardKind, liveDecisionRefusal } from "../shared/live-approval.js";
|
|
62
65
|
import { boatAccountResourceChangeError, cloudBackendChangeError, vpsAliasResourceChangeError, } from "./cloud-backend.js";
|
|
63
66
|
import * as composio from "./composio.js";
|
|
64
67
|
import { connectorCallFromFrame, connectorRefusalText, connectorUnrecognizedText, evaluateConnectorTools } from "./connector-verdict.js";
|
|
@@ -69,7 +72,7 @@ import { appendAboutMe, commitLearned, planLearned } from "./profile-learned.js"
|
|
|
69
72
|
import { canAccessTeam, canReachPeer, coordinatorSupervises, livePeerRoster, livePeerRosterBlock, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, resolveTeammate, roomPeerRosterSystemPrompt, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
|
|
70
73
|
import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerExec, containerRuntimeStatus, localVmWakeAction, localVmWorkspaceExists, perBotLocalVmTarget, poolLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, VM_WORKSPACE_GUEST, } from "./container-computer.js";
|
|
71
74
|
import { attachForTurn, saveBotAttachment } from "./bot-attachment.js";
|
|
72
|
-
import { cacheUntilConfigChanges, ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmIdleTimeoutMinutes, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, routinesInConversationEnabled, claudeUserMcpEnabled, skillAuthoringEnabled, autoRecallEnabled, captureQuietMs, tidyHour, sharedComputersEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled,
|
|
75
|
+
import { cacheUntilConfigChanges, ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmIdleTimeoutMinutes, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, routinesInConversationEnabled, claudeUserMcpEnabled, skillAuthoringEnabled, autoRecallEnabled, captureQuietMs, tidyHour, sharedComputersEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, stripWorkspaceCredentialEnv, PROVIDER_CREDENTIAL_ENV, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, skillsLibraryEnabled, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, CLAUDE_API_INSTANCE, liveSettingsFor, mergeOpenCodeProviderKeys, openCodeProviderKeys, } from "./config.js";
|
|
73
76
|
import { sweepThreadEventLogs } from "./thread-retention.js";
|
|
74
77
|
import { ComputerControl } from "./computer-control.js";
|
|
75
78
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
@@ -85,16 +88,16 @@ import { entitled } from "./enterprise.js";
|
|
|
85
88
|
import { HOSTED_CONTRACT_HEADER, HOSTED_CONTRACT_METADATA, HOSTED_CONTRACT_VERSION } from "./hosted-contract.js";
|
|
86
89
|
import { describeSpawnFailure, execCli } from "./procs.js";
|
|
87
90
|
import { blockedTarget, buildNotification, buildSpendNotification } from "./notify.js";
|
|
88
|
-
import { isModelVariant, TurnNotStartedError, newId,
|
|
91
|
+
import { isModelVariant, TurnNotStartedError, newId, } from "./contracts.js";
|
|
89
92
|
import { RETRY_MAX_ATTEMPTS } from "./drivers/retry.js";
|
|
90
93
|
import { recoveryCapabilityError } from "./automatic-recovery.js";
|
|
91
94
|
import { decodeGeneratedImage } from "./generated-image.js";
|
|
92
|
-
import { MAX_MCP_SERVERS, isRemoteMcpServer, listMcpServers, mcpServerNameError, parseMcpServerMutation, parseMcpServersImport, parseStoredMcpServer, } from "./mcp-registry.js";
|
|
95
|
+
import { addDisabledMcpServer, MAX_MCP_SERVERS, isRemoteMcpServer, listMcpServers, mcpServerNameError, parseMcpServerMutation, parseMcpServersImport, parseStoredMcpServer, } from "./mcp-registry.js";
|
|
93
96
|
import { McpOAuthError, McpSignInError, McpOAuthManager, mcpOAuthRedirectUri, withMcpSignIn, withoutPendingSignIn } from "./mcp-oauth.js";
|
|
94
97
|
import { probeMcpServer } from "./mcp-probe.js";
|
|
95
98
|
import { GROUP_GOAL_MAX_TURNS, groupGoalAssignmentKey, groupGoalCompletionTurnId, groupGoalCoordinatorInstructions, groupGoalWorkerInstructions, parseGroupGoalDecision, resolveGroupGoalMember, selectGroupGoalCoordinator, } from "./group-goal-run.js";
|
|
96
99
|
import { BUILT_IN_DRIVERS } from "./drivers/builtIn.js";
|
|
97
|
-
import { openCodeProviderKeysAllowed, setOpenCodeProviderKeyPolicy } from "./drivers/acp/opencode-go.js";
|
|
100
|
+
import { openCodeProviderKeysAllowed, setOpenCodeOwnProviderKeys, setOpenCodeProviderKeyPolicy } from "./drivers/acp/opencode-go.js";
|
|
98
101
|
import { getOrCreateChannel, mirrorActivity, mirrorExchange, mirrorReply } from "./comms-visibility.js";
|
|
99
102
|
import { readMessageText, recallMessages, recentMessages, searchMessagesAsync, closeMessageSearch, closeMessageDb, chatFollowups, cancelledChatFollowup, settleChatFollowups, threadsReferencing } from "./message-db.js";
|
|
100
103
|
import { briefCrossingLabel, claimRecallCrossings, recallCrossingLabel } from "./recall-disclosure.js";
|
|
@@ -115,9 +118,10 @@ import { ProviderRegistry } from "./harness/registry.js";
|
|
|
115
118
|
import { ManagedDesktopProviders } from "./managed-desktop.js";
|
|
116
119
|
import { computerKindForResource, ManagedDesktopPolicy } from "./managed-policy.js";
|
|
117
120
|
import { hostedModelPolicy, HOSTED_MODEL_POLICY_HEADER, HOSTED_PROVIDER_SETTINGS_ERROR } from "./hosted-models.js";
|
|
118
|
-
import { boatNotConfiguredMessage,
|
|
121
|
+
import { boatNotConfiguredMessage, CLOUD_HOME_SECRET_KEYS, CLOUD_IGNORED_KEYS, CLOUD_PAIRING_PATH, cloudHomeConfiguration, cloudHomeOffersPlace, cloudHomePlaceRefusal, createCloudPairing, firstCloudTurnPatch, readSignedBody, } from "./cloud-home.js";
|
|
119
122
|
import { CLOUD_PERSONAL_REFUSAL, settleCloudOwnership } from "./cloud-owner.js";
|
|
120
|
-
import { createCloudMoveRoutes } from "./cloud-move-http.js";
|
|
123
|
+
import { createCloudMoveRoutes, workspaceShared } from "./cloud-move-http.js";
|
|
124
|
+
import { RESTART_EXIT_CODE } from "./restart.js";
|
|
121
125
|
import { holdIncludedServices } from "./included-services.js";
|
|
122
126
|
import { selectDefaultModelSelection, withNewBotEffort } from "./default-model-selection.js";
|
|
123
127
|
import { cancelPeerApprovalsFor, cancelPeerApprovalsForThread, dismissStalePeerCards, peerApprovalFailure, requestPeerApproval, resolvePeerComms } from "./peer-approval.js";
|
|
@@ -137,8 +141,7 @@ import { Handoffs, handedStateUsable, recordHanded, renderUnseen, sessionStart,
|
|
|
137
141
|
import { extractTurnImages } from "./turn-images.js";
|
|
138
142
|
import { threadTitlePrompt, titleConversationExcerpt } from "./thread-title.js";
|
|
139
143
|
import { TurnWatchdog } from "./turn-watchdog.js";
|
|
140
|
-
import { TurnResources
|
|
141
|
-
import { IdleReleasePolicy } from "./claim-idle.js";
|
|
144
|
+
import { TurnResources } from "./turn-resources.js";
|
|
142
145
|
import { ensureWorkspace, ensureTaskWorkspace, workspaceLocationsPrompt, supportsWorkspaceFiles, updateMemory, appendMemoryLog, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, TASK_WORKSPACES_DIR, readMemoryOnlyFromRegularFiles, workspaceDir, } from "./workspace.js";
|
|
143
146
|
import { listMemoryTopics, memoryDate, readMemoryFile, readMemoryTopic, writeMemoryFile, writeMemoryTopic } from "./workspace.js";
|
|
144
147
|
import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, journalMemoryWrite, } from "./memory-journal.js";
|
|
@@ -150,7 +153,7 @@ import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefault
|
|
|
150
153
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
151
154
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
152
155
|
import { checkSoulDrift, readSoulDrift, soulFile, writeSoulMirror } from "./bot-folder.js";
|
|
153
|
-
import { buildSystemPrompt, userProfileSystemPrompt, cloudHomePrompt, computerPrompt, composioSystemPrompt, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, TEAM_MEMORY_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, resolveComputerPromptKind, } from "./system-prompt.js";
|
|
156
|
+
import { buildSystemPrompt, userProfileSystemPrompt, cloudHomePrompt, computerPrompt, composioSystemPrompt, customMcpPrompt, CREDENTIAL_PROMPT, mentionPrompt, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, TEAM_MEMORY_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, resolveComputerPromptKind, teamAvailabilityPart, } from "./system-prompt.js";
|
|
154
157
|
import { readCuaConnection, readCuaUnavailableReason, gatedLocalComputer } from "./local-computer.js";
|
|
155
158
|
import { discoverExistingPerBotLocalVms, discoverExistingPoolLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
156
159
|
import { localVmStopReason, recordLocalVmIdleStop } from "./local-vm-stop-reason.js";
|
|
@@ -173,7 +176,6 @@ import { createOptionsCard } from "./options-card.js";
|
|
|
173
176
|
import { buildBotOverview, connectedAppsFacts } from "./bot-overview.js";
|
|
174
177
|
import { ProfileRequestService } from "./profile-requests.js";
|
|
175
178
|
import { ModelRequestService } from "./model-requests.js";
|
|
176
|
-
import { TighteningRequestService } from "./tightening-requests.js";
|
|
177
179
|
import { TeamSetupError, TeamSetupRequestService } from "./team-setup-requests.js";
|
|
178
180
|
import { profileRevision, profileSnapshot } from "./profile-revision.js";
|
|
179
181
|
import { flushAllProfileHistory, flushProfileHistory, readHistory, recordProfileChange } from "./profile-versions.js";
|
|
@@ -208,7 +210,6 @@ import { shouldMountLocalComputer } from "./local-routing.js";
|
|
|
208
210
|
import { autoLocalVmAttachable } from "./container-computer.js";
|
|
209
211
|
import { startAutoVmClaim } from "./auto-vm-claims.js";
|
|
210
212
|
import { computerFreeAfterText, computerParkedText, computerStoppedWaitingText, computerWaitingText } from "./computer-wait.js";
|
|
211
|
-
import { CloudOverflowConsent, CloudSeatLease, cloudOverflowAction, cloudOverflowConsentText, cloudOverflowOfferText, cloudSeatStartedText, cloudSeatStoppedText } from "./cloud-overflow.js";
|
|
212
213
|
import { modelContextWindow } from "./model-context-window.js";
|
|
213
214
|
import { cloudPlaceDriverError, parseSurface, PlaceUnavailableError, placeUnavailable, resolveSurface, surfaceLabel, surfaceOfComputerKind, surfacePrompt } from "./surface.js";
|
|
214
215
|
import { PendingTurnCancellations, ProviderTurnGenerationRegistry, RetiredTurnRegistry, guardTurnDispatch, isTurnAdmissionBlocked, isTurnEventQuarantined, } from "./turn-dispatch-guard.js";
|
|
@@ -775,11 +776,16 @@ function approvalShape(shape) {
|
|
|
775
776
|
/** Who opens a routine's results conversation on a Cloud home: the writer
|
|
776
777
|
* of this request, else the owner for a routine that is theirs (they wrote
|
|
777
778
|
* it as it stands, or they are its writer: cloud-owner.ts), else its last
|
|
778
|
-
* writer, else nobody.
|
|
779
|
+
* writer, else nobody. A webhook's run is the owner's: only their own
|
|
780
|
+
* devices can create, edit or rotate one (admin scope), so it runs at the
|
|
781
|
+
* bot's own level, in its folder, as on the desktop. Its payload still never
|
|
782
|
+
* reaches the lent Mac: cloud-lending.ts refuses every webhook run. */
|
|
779
783
|
function routineOpener(routineId) {
|
|
780
784
|
if (routineWriterInFlight)
|
|
781
785
|
return routineWriterInFlight;
|
|
782
786
|
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;
|
|
783
789
|
if (routine && CLOUD_OWNER_KEY && cloudRoutineAuthors?.authored(routineId, routine))
|
|
784
790
|
return CLOUD_OWNER_KEY;
|
|
785
791
|
const writer = cloudRoutineAuthors?.writer(routineId);
|
|
@@ -1247,11 +1253,11 @@ function memberFrameContext(visible) {
|
|
|
1247
1253
|
webhookBot: (webhookId) => webhooks.list().find((webhook) => webhook.id === webhookId)?.botId,
|
|
1248
1254
|
freshBot: (botId) => {
|
|
1249
1255
|
const bot = store.bot(botId);
|
|
1250
|
-
return bot ?
|
|
1256
|
+
return bot ? publicBot(bot) : undefined;
|
|
1251
1257
|
},
|
|
1252
1258
|
freshGroup: (groupId) => {
|
|
1253
1259
|
const group = store.group(groupId);
|
|
1254
|
-
return group ?
|
|
1260
|
+
return group ? groupWithThread(group) : undefined;
|
|
1255
1261
|
},
|
|
1256
1262
|
};
|
|
1257
1263
|
}
|
|
@@ -1522,6 +1528,8 @@ const openCodeKeysAllowed = () => openCodeProviderKeysAllowed({
|
|
|
1522
1528
|
sharedSignIn: sharedSignIn(signInAllowList()),
|
|
1523
1529
|
});
|
|
1524
1530
|
setOpenCodeProviderKeyPolicy(openCodeKeysAllowed);
|
|
1531
|
+
// Keys the owner saved for OpenCode in Settings reach it on every server.
|
|
1532
|
+
setOpenCodeOwnProviderKeys(() => openCodeProviderKeys(cfg));
|
|
1525
1533
|
let openCodeKeysLastAllowed = openCodeKeysAllowed();
|
|
1526
1534
|
/** OpenCode lists a provider's models only while it may read that
|
|
1527
1535
|
* provider's key. When enrollment or the sign-in list changes the answer,
|
|
@@ -1754,6 +1762,8 @@ utilityParentPort?.on("message", event => {
|
|
|
1754
1762
|
});
|
|
1755
1763
|
});
|
|
1756
1764
|
const MAX_ATTACHED_FILES_PER_TURN = 10;
|
|
1765
|
+
const MAX_ROOM_POSTS_PER_TURN = 3;
|
|
1766
|
+
const MAX_CREATED_BOTS_PER_TURN = 4;
|
|
1757
1767
|
// A capability lives for the exact provider-turn generation, including while
|
|
1758
1768
|
// that turn is parked on a human approval. The long ceiling is only an orphan
|
|
1759
1769
|
// backstop for an impossible-to-settle adapter; normal terminal paths revoke
|
|
@@ -1810,15 +1820,52 @@ function beginInternalCapabilityGeneration(threadId, generation = randomUUID())
|
|
|
1810
1820
|
activeInternalGenerationByThread.set(threadId, generation);
|
|
1811
1821
|
return generation;
|
|
1812
1822
|
}
|
|
1813
|
-
|
|
1823
|
+
// The bearer an integration's proxy holds is part of the engine's launch
|
|
1824
|
+
// contract (Claude's process key, ACP's session inputs): a new one every turn
|
|
1825
|
+
// meant a relaunch, a resumed transcript and new MCP servers every turn. So
|
|
1826
|
+
// the bearer stays the same while the same bot works the same thread with the
|
|
1827
|
+
// same grants and approval level, and only the record behind it is per turn:
|
|
1828
|
+
// between turns, and after any revoke, nothing honours it. A changed grant,
|
|
1829
|
+
// Stop, a stall, a deleted bot or thread or a provider reload gives the next
|
|
1830
|
+
// turn a new bearer, so whatever still holds the old one is never honoured
|
|
1831
|
+
// again.
|
|
1832
|
+
/** threadId → "botId kind" → the bearer and the grants it was minted for. */
|
|
1833
|
+
const sessionCredentials = new Map();
|
|
1834
|
+
/** What a bearer stands for: everything its record grants (a turn's counters
|
|
1835
|
+
* all start at zero), plus the thread's approval level; not the turn. */
|
|
1836
|
+
function sessionGrants({ generation: _turn, ...grants }) {
|
|
1837
|
+
const thread = botForThread(grants.botId, grants.threadId);
|
|
1838
|
+
return JSON.stringify({ ...grants, approval: thread ? approvalModeFor(thread) : null });
|
|
1839
|
+
}
|
|
1840
|
+
/** The thread's bearer for this integration, new only when its grants changed. */
|
|
1841
|
+
function sessionBearer(capability) {
|
|
1842
|
+
const credentials = sessionCredentials.get(capability.threadId) ?? new Map();
|
|
1843
|
+
sessionCredentials.set(capability.threadId, credentials);
|
|
1844
|
+
const slot = `${capability.botId} ${capability.kind}`;
|
|
1845
|
+
const grants = sessionGrants(capability);
|
|
1846
|
+
const held = credentials.get(slot);
|
|
1847
|
+
if (held?.grants === grants)
|
|
1848
|
+
return held.token;
|
|
1849
|
+
const token = randomBytes(24).toString("hex");
|
|
1850
|
+
credentials.set(slot, { grants, token });
|
|
1851
|
+
return token;
|
|
1852
|
+
}
|
|
1853
|
+
function mintInternalCapability(capability, { perTurn = false } = {}) {
|
|
1814
1854
|
if (activeInternalGenerationByThread.get(capability.threadId) !== capability.generation) {
|
|
1815
1855
|
throw new Error("cannot mint an integration capability for an inactive turn");
|
|
1816
1856
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1857
|
+
// perTurn: a bridge into a machine the harness stops and starts between
|
|
1858
|
+
// turns (a Local VM, the VPS, a Boat). A warm engine never restarts a
|
|
1859
|
+
// server whose machine went away, so such a turn gets a new bearer and
|
|
1860
|
+
// starts its engine fresh.
|
|
1861
|
+
const token = perTurn ? randomBytes(24).toString("hex") : sessionBearer(capability);
|
|
1862
|
+
// Minted twice in one turn: keep that turn's record and its counters.
|
|
1863
|
+
if (internalCapabilities.get(token)?.generation !== capability.generation) {
|
|
1864
|
+
internalCapabilities.set(token, {
|
|
1865
|
+
...capability,
|
|
1866
|
+
orphanExpiresAt: Date.now() + INTERNAL_CAPABILITY_ORPHAN_MS,
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1822
1869
|
return token;
|
|
1823
1870
|
}
|
|
1824
1871
|
/** A turn that was not provably the owner's has ended: judge its bot's
|
|
@@ -1852,7 +1899,9 @@ function revokeInternalCapabilityGeneration(threadId, generation) {
|
|
|
1852
1899
|
}
|
|
1853
1900
|
internalGenerationByProviderTurn.deleteGeneration(threadId, generation);
|
|
1854
1901
|
}
|
|
1855
|
-
|
|
1902
|
+
/** Before a new turn on this thread mints: nothing an earlier turn here was
|
|
1903
|
+
* given stays honoured. The thread's bearers stay, so its engine stays warm. */
|
|
1904
|
+
function revokeEarlierTurnCapabilities(threadId) {
|
|
1856
1905
|
computerSelectionTurns.delete(threadId);
|
|
1857
1906
|
const generation = activeInternalGenerationByThread.get(threadId);
|
|
1858
1907
|
if (generation)
|
|
@@ -1865,9 +1914,16 @@ function revokeInternalCapabilitiesForThread(threadId) {
|
|
|
1865
1914
|
internalCapabilities.delete(token);
|
|
1866
1915
|
}
|
|
1867
1916
|
}
|
|
1917
|
+
/** Stop, a stall, a deleted bot or thread, a provider change: the thread's
|
|
1918
|
+
* bearers are retired too, so nothing that still holds one is honoured again. */
|
|
1919
|
+
function revokeInternalCapabilitiesForThread(threadId) {
|
|
1920
|
+
revokeEarlierTurnCapabilities(threadId);
|
|
1921
|
+
sessionCredentials.delete(threadId);
|
|
1922
|
+
}
|
|
1868
1923
|
function revokeAllInternalCapabilities() {
|
|
1869
1924
|
computerSelectionTurns.clear();
|
|
1870
1925
|
internalCapabilities.clear();
|
|
1926
|
+
sessionCredentials.clear();
|
|
1871
1927
|
// foreignTurns stays: a turn that is not provably the owner's can still
|
|
1872
1928
|
// write while its engine is torn down, and its own end (which revokes its
|
|
1873
1929
|
// generation by id) is what judges it (server/lending-memory.ts).
|
|
@@ -2031,6 +2087,15 @@ class ComputerWaitParked extends Error {
|
|
|
2031
2087
|
this.generation = generation;
|
|
2032
2088
|
}
|
|
2033
2089
|
}
|
|
2090
|
+
/** A place the turn was told to use failed to attach: its cause plus the one
|
|
2091
|
+
* control that changes it (placeUnavailable). A cancelled setup, a parked
|
|
2092
|
+
* computer wait and an already-worded place failure pass through as they
|
|
2093
|
+
* are. The one wrapper for a bot thread and a room turn. */
|
|
2094
|
+
function asPlaceFailure(error, place, source, limit) {
|
|
2095
|
+
if (error instanceof DirectTurnSetupCancelled || error instanceof ComputerWaitParked || error instanceof PlaceUnavailableError)
|
|
2096
|
+
return error;
|
|
2097
|
+
return placeUnavailable(place, source, error instanceof Error ? error.message : String(error), limit);
|
|
2098
|
+
}
|
|
2034
2099
|
const directTurnDispatchClaims = new Map();
|
|
2035
2100
|
const directTurnGenerationByThread = new Map();
|
|
2036
2101
|
// Only the latest direct request per thread is retained. A fresh user turn
|
|
@@ -2073,173 +2138,14 @@ const turnResources = new TurnResources();
|
|
|
2073
2138
|
const sharedComputerControl = new SharedComputerControl(turnResources, () => store.bots.some(bot => botComputerControlSnapshot(bot.id).held));
|
|
2074
2139
|
const turnResourceOwners = new Map();
|
|
2075
2140
|
const turnComputerResources = new Map();
|
|
2076
|
-
/** Screen-touching computer calls that started before their turn's computer
|
|
2077
|
-
* claim was bound (#1653 over the lazy attach of #1361): the start event
|
|
2078
|
-
* finds no claim to fence, so it waits here and bindTurnComputer carries it
|
|
2079
|
-
* into the fresh claim, keeping the quiet window from releasing a seat
|
|
2080
|
-
* under a call the desktop is already executing. */
|
|
2081
|
-
const pendingComputerCallStarts = new Map();
|
|
2082
2141
|
const teamComputerTurns = new Map();
|
|
2083
2142
|
const settlingResourceOwners = new Map();
|
|
2084
|
-
/** Idle release for computer claims (#1653), disabled until the feature
|
|
2085
|
-
* flag is on: a seat that stays screen-quiet for the policy's window is
|
|
2086
|
-
* released to waiting turns while its holder's turn still lives, and the
|
|
2087
|
-
* previous holder re-claims directly inside the reclaim window — it
|
|
2088
|
-
* yields once another turn holds the seat. The durable VM profile keeps
|
|
2089
|
-
* its login state, so that re-claim is cheap. Read per claim: a flag
|
|
2090
|
-
* change applies to new claims, never to a seat held under the old
|
|
2091
|
-
* setting. computer:phone is excluded: it is the #1663 exclusivity lock,
|
|
2092
|
-
* claimed once per turn with no activity signal to renew it. */
|
|
2093
|
-
const COMPUTER_CLAIM_IDLE_POLICY = new IdleReleasePolicy();
|
|
2094
|
-
function computerClaimIdlePolicy(resource) {
|
|
2095
|
-
return resource.startsWith("computer:") && resource !== "computer:phone" && computerClaimIdleReleaseEnabled(cfg) ? COMPUTER_CLAIM_IDLE_POLICY : undefined;
|
|
2096
|
-
}
|
|
2097
|
-
/** #1655: the consent ledger for local-wait cloud overflow, and the live
|
|
2098
|
-
* cloud seats it has started. One seat per bot — it is the bot's own Box,
|
|
2099
|
-
* shared by that bot's threads the way every other computer is. */
|
|
2100
|
-
const cloudOverflowConsent = new CloudOverflowConsent();
|
|
2101
|
-
const cloudSeatLeases = new Map();
|
|
2102
|
-
let cloudSeatSweepTimer;
|
|
2103
|
-
/** One stop attempt per seat at a time (#1655): the sweep ticks faster
|
|
2104
|
-
* than a Box sleeps, and a second stop call would race the first. */
|
|
2105
|
-
const cloudSeatStopsInFlight = new Set();
|
|
2106
|
-
/** The waits that may overflow: a turn queued for a local desktop, never
|
|
2107
|
-
* one already bound for a cloud or VPS seat. */
|
|
2108
|
-
function localComputerWait(resource) {
|
|
2109
|
-
return resource === "computer:host" || resource.startsWith("computer:vm:");
|
|
2110
|
-
}
|
|
2111
|
-
function cloudOverflowSituation(owner, resource, started) {
|
|
2112
|
-
if (!localComputerWait(resource))
|
|
2113
|
-
return null;
|
|
2114
|
-
if (!store.botByThread(owner.threadId))
|
|
2115
|
-
return null;
|
|
2116
|
-
const perSecondCostUsd = cloudOverflowPerSecondCostUsd(cfg);
|
|
2117
|
-
return {
|
|
2118
|
-
featureEnabled: cloudOverflowEnabled(cfg),
|
|
2119
|
-
// An organisation that refuses the Box kind has nothing to offer, and
|
|
2120
|
-
// the decision must fail closed before any card is written.
|
|
2121
|
-
cloudConfigured: boat.boatConfigured(cfg) && managedPolicy.computerRefusal("box") === undefined,
|
|
2122
|
-
perSecondCostUsd,
|
|
2123
|
-
// Consent answers the priced card (#1655): a grant counts only at the
|
|
2124
|
-
// rate its card showed, so a config change re-offers instead of
|
|
2125
|
-
// starting at a price nobody approved.
|
|
2126
|
-
consented: cloudOverflowConsent.consented(owner.threadId, cloudOverflowAllowlistedThreads(cfg), perSecondCostUsd ?? undefined),
|
|
2127
|
-
offered: cloudOverflowConsent.offered(owner.threadId, perSecondCostUsd ?? undefined),
|
|
2128
|
-
started,
|
|
2129
|
-
};
|
|
2130
|
-
}
|
|
2131
2143
|
function claimTurnResource(owner, resource) {
|
|
2132
|
-
|
|
2133
|
-
if (!turnResources.claim(resource, owner, idle ? { idle } : {}))
|
|
2144
|
+
if (!turnResources.claim(resource, owner))
|
|
2134
2145
|
return false;
|
|
2135
2146
|
turnResourceOwners.set(owner.threadId, owner);
|
|
2136
2147
|
return true;
|
|
2137
2148
|
}
|
|
2138
|
-
/** Start the waiting bot's cloud seat (#1655). True once handled — started
|
|
2139
|
-
* or terminally failed — so the wait never retries every poll; false when
|
|
2140
|
-
* another turn of this bot owns the Box lifecycle and a later poll may
|
|
2141
|
-
* still land. */
|
|
2142
|
-
async function startCloudSeat(owner) {
|
|
2143
|
-
const bot = store.botByThread(owner.threadId);
|
|
2144
|
-
if (!bot)
|
|
2145
|
-
return true;
|
|
2146
|
-
const perSecondCostUsd = cloudOverflowPerSecondCostUsd(cfg);
|
|
2147
|
-
if (!perSecondCostUsd)
|
|
2148
|
-
return true;
|
|
2149
|
-
const boxBotResource = "computer:box-bot:" + bot.id;
|
|
2150
|
-
if (!claimTurnResource(owner, boxBotResource))
|
|
2151
|
-
return false;
|
|
2152
|
-
try {
|
|
2153
|
-
broadcast({ kind: "computer", botId: bot.id, state: "waking" });
|
|
2154
|
-
const machine = await boat.readyBoat(cfg, bot.id);
|
|
2155
|
-
if (!machine)
|
|
2156
|
-
throw new Error("the cloud computer did not wake");
|
|
2157
|
-
// Consent can be revoked while the Box wakes (#1655): a seat nobody
|
|
2158
|
-
// consented to must not bill. It is released and put straight back to
|
|
2159
|
-
// sleep — and if even that fails, leased as already idle so the sweep
|
|
2160
|
-
// keeps retrying with backoff.
|
|
2161
|
-
if (!cloudOverflowConsent.consented(owner.threadId, cloudOverflowAllowlistedThreads(cfg), perSecondCostUsd)) {
|
|
2162
|
-
turnResources.releaseOne(boxBotResource, owner);
|
|
2163
|
-
const idleStopMs = cloudOverflowIdleStopMs(cfg);
|
|
2164
|
-
void boat.sleepBoat(cfg, bot.id).catch(() => {
|
|
2165
|
-
cloudSeatLeases.set(bot.id, new CloudSeatLease({ botId: bot.id, threadId: owner.threadId, generation: owner.generation, now: Date.now() - idleStopMs - 1, idleStopMs }));
|
|
2166
|
-
ensureCloudSeatSweep();
|
|
2167
|
-
});
|
|
2168
|
-
store.appendMessage(owner.threadId, {
|
|
2169
|
-
role: "bot", kind: "activity",
|
|
2170
|
-
tool: { name: "Cloud overflow consent was revoked while the cloud computer woke; it stops unbilled and the local wait continues.", ok: true },
|
|
2171
|
-
});
|
|
2172
|
-
return true;
|
|
2173
|
-
}
|
|
2174
|
-
cloudSeatLeases.set(bot.id, new CloudSeatLease({ botId: bot.id, threadId: owner.threadId, generation: owner.generation, idleStopMs: cloudOverflowIdleStopMs(cfg) }));
|
|
2175
|
-
ensureCloudSeatSweep();
|
|
2176
|
-
store.appendMessage(owner.threadId, {
|
|
2177
|
-
role: "bot", kind: "activity",
|
|
2178
|
-
tool: { name: cloudSeatStartedText({ perSecondCostUsd, idleStopMs: cloudOverflowIdleStopMs(cfg) }), ok: true },
|
|
2179
|
-
});
|
|
2180
|
-
return true;
|
|
2181
|
-
}
|
|
2182
|
-
catch (error) {
|
|
2183
|
-
turnResources.releaseOne(boxBotResource, owner);
|
|
2184
|
-
store.appendMessage(owner.threadId, {
|
|
2185
|
-
role: "bot", kind: "activity",
|
|
2186
|
-
tool: { name: "Cloud computer could not start (" + (error instanceof Error ? error.message : String(error)) + "); the local wait continues.", ok: false },
|
|
2187
|
-
});
|
|
2188
|
-
return true;
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
/** A per-second-billed seat must stop on idleness even when nothing else
|
|
2192
|
-
* touches the server, so this slice carries the one timer — unref'd, it
|
|
2193
|
-
* can never hold the process open, and it dies with the last lease. */
|
|
2194
|
-
function ensureCloudSeatSweep() {
|
|
2195
|
-
if (cloudSeatSweepTimer)
|
|
2196
|
-
return;
|
|
2197
|
-
cloudSeatSweepTimer = setInterval(() => {
|
|
2198
|
-
sweepIdleCloudSeats();
|
|
2199
|
-
if (!cloudSeatLeases.size && cloudSeatSweepTimer) {
|
|
2200
|
-
clearInterval(cloudSeatSweepTimer);
|
|
2201
|
-
cloudSeatSweepTimer = undefined;
|
|
2202
|
-
}
|
|
2203
|
-
}, 15_000);
|
|
2204
|
-
cloudSeatSweepTimer.unref?.();
|
|
2205
|
-
}
|
|
2206
|
-
/** Stop every cloud seat idle past its window (#1655): the machine is
|
|
2207
|
-
* archived, billing pauses, and the transcript says which wait's seat it
|
|
2208
|
-
* was. Only real computer tool completions refresh a lease — the poller's
|
|
2209
|
-
* preview frames never reach the touch call. */
|
|
2210
|
-
/** Stop one running cloud seat (#1655): the Box sleeps, and only a
|
|
2211
|
-
* successful stop clears the lease and its Box claim — a failed stop
|
|
2212
|
-
* stays leased and retryable with backoff, because the seat still bills
|
|
2213
|
-
* until it sleeps. The claim releases for the lease's own generation,
|
|
2214
|
-
* never a newer turn's. */
|
|
2215
|
-
function stopCloudSeat(botId, lease, stoppedName, now = Date.now()) {
|
|
2216
|
-
if (cloudSeatStopsInFlight.has(botId))
|
|
2217
|
-
return;
|
|
2218
|
-
if (cloudSeatLeases.get(botId) !== lease)
|
|
2219
|
-
return;
|
|
2220
|
-
cloudSeatStopsInFlight.add(botId);
|
|
2221
|
-
void boat.sleepBoat(cfg, botId)
|
|
2222
|
-
.then(() => {
|
|
2223
|
-
if (cloudSeatLeases.get(botId) === lease)
|
|
2224
|
-
cloudSeatLeases.delete(botId);
|
|
2225
|
-
// The stopped seat's Box claim goes with it, so a still-waiting
|
|
2226
|
-
// turn may start a fresh seat for the same bot.
|
|
2227
|
-
turnResources.releaseOne(`computer:box-bot:${botId}`, { threadId: lease.threadId, generation: lease.generation });
|
|
2228
|
-
store.appendMessage(lease.threadId, { role: "bot", kind: "activity", tool: { name: stoppedName, ok: true } });
|
|
2229
|
-
})
|
|
2230
|
-
.catch(() => {
|
|
2231
|
-
lease.deferSleep(now);
|
|
2232
|
-
store.appendMessage(lease.threadId, { role: "bot", kind: "activity", tool: { name: "Cloud computer could not stop — the sweep retries with backoff; stop it from Settings if it stays awake. Billing continues until it sleeps.", ok: false } });
|
|
2233
|
-
})
|
|
2234
|
-
.finally(() => cloudSeatStopsInFlight.delete(botId));
|
|
2235
|
-
}
|
|
2236
|
-
function sweepIdleCloudSeats(now = Date.now()) {
|
|
2237
|
-
for (const [botId, lease] of cloudSeatLeases) {
|
|
2238
|
-
if (!lease.idleElapsed(now) || !lease.sleepDue(now))
|
|
2239
|
-
continue;
|
|
2240
|
-
stopCloudSeat(botId, lease, cloudSeatStoppedText(lease.idleFor(now)), now);
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
2149
|
function releaseTurnResources(owner) {
|
|
2244
2150
|
if (!owner)
|
|
2245
2151
|
return;
|
|
@@ -2252,8 +2158,6 @@ function releaseTurnResources(owner) {
|
|
|
2252
2158
|
turnResourceOwners.delete(owner.threadId);
|
|
2253
2159
|
if (turnComputerResources.get(owner.threadId)?.owner.generation === owner.generation)
|
|
2254
2160
|
turnComputerResources.delete(owner.threadId);
|
|
2255
|
-
if (pendingComputerCallStarts.get(owner.threadId)?.generation === owner.generation)
|
|
2256
|
-
pendingComputerCallStarts.delete(owner.threadId);
|
|
2257
2161
|
const teamTurn = teamComputerTurns.get(owner.threadId);
|
|
2258
2162
|
if (teamTurn?.owner.generation === owner.generation) {
|
|
2259
2163
|
stopScreenPoller(teamTurn.botId, owner.threadId);
|
|
@@ -2283,10 +2187,6 @@ async function bindTurnComputer(owner, resource, exclusive = false) {
|
|
|
2283
2187
|
// turn.wait_* events carry the same facts for the inspector log.
|
|
2284
2188
|
let waitedSince;
|
|
2285
2189
|
let waitEnded = false;
|
|
2286
|
-
// #1655: whether this wait already started (or terminally failed to
|
|
2287
|
-
// start) a cloud seat; the decision is re-evaluated every poll because
|
|
2288
|
-
// consent can arrive mid-wait.
|
|
2289
|
-
let overflowHandled = false;
|
|
2290
2190
|
const waitEventBase = () => {
|
|
2291
2191
|
const selection = store.botByThread(owner.threadId)?.modelSelection;
|
|
2292
2192
|
const instance = selection ? registry.get(selection.instanceId) : null;
|
|
@@ -2360,32 +2260,7 @@ async function bindTurnComputer(owner, resource, exclusive = false) {
|
|
|
2360
2260
|
position,
|
|
2361
2261
|
...(holder ? { holder } : {}),
|
|
2362
2262
|
});
|
|
2363
|
-
// #1655: the wait is where an overflow choice gets made. The card
|
|
2364
|
-
// names the per-second cost before anything can start, and only a
|
|
2365
|
-
// turn genuinely waiting on a local desktop gets one. Fail closed:
|
|
2366
|
-
// feature off, cloud unusable, or no operator-set rate means no
|
|
2367
|
-
// card at all — Auto never offers, never starts.
|
|
2368
|
-
const overflow = cloudOverflowSituation(owner, resource, overflowHandled);
|
|
2369
|
-
const perSecondCostUsd = overflow?.perSecondCostUsd ?? null;
|
|
2370
|
-
if (overflow && perSecondCostUsd !== null && cloudOverflowAction(overflow).kind === "offer") {
|
|
2371
|
-
store.appendMessage(owner.threadId, {
|
|
2372
|
-
role: "bot", kind: "activity",
|
|
2373
|
-
tool: { name: cloudOverflowOfferText({ perSecondCostUsd, idleStopMs: cloudOverflowIdleStopMs(cfg) }), ok: true },
|
|
2374
|
-
});
|
|
2375
|
-
cloudOverflowConsent.markOffered(owner.threadId, perSecondCostUsd);
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
// #1655: consent can arrive mid-wait — the card's answer, or a
|
|
2379
|
-
// thread the operator allowlisted — and only then may the cloud
|
|
2380
|
-
// seat start, its cost chip landing beside the waiting one. The
|
|
2381
|
-
// local wait itself is untouched: it still ends by acquisition,
|
|
2382
|
-
// stop, or the ceiling (parking, once that stack lands).
|
|
2383
|
-
if (!overflowHandled) {
|
|
2384
|
-
const situation = cloudOverflowSituation(owner, resource, overflowHandled);
|
|
2385
|
-
if (situation && cloudOverflowAction(situation).kind === "start")
|
|
2386
|
-
overflowHandled = await startCloudSeat(owner);
|
|
2387
2263
|
}
|
|
2388
|
-
sweepIdleCloudSeats();
|
|
2389
2264
|
if (Date.now() >= deadline) {
|
|
2390
2265
|
// ADR-2 (#1651): the ceiling parks the turn — settled, with a
|
|
2391
2266
|
// registered resume — instead of failing it. The owner mark lets the
|
|
@@ -2407,15 +2282,6 @@ async function bindTurnComputer(owner, resource, exclusive = false) {
|
|
|
2407
2282
|
}
|
|
2408
2283
|
turnResourceOwners.set(owner.threadId, owner);
|
|
2409
2284
|
turnComputerResources.set(owner.threadId, { owner, resource });
|
|
2410
|
-
// A screen call that started before this bind (#1653) fenced nothing
|
|
2411
|
-
// because no claim existed yet: carry its pending start into the fresh
|
|
2412
|
-
// claim so the idle window still defers to the running call.
|
|
2413
|
-
const pendingStarts = pendingComputerCallStarts.get(owner.threadId);
|
|
2414
|
-
if (pendingStarts?.generation === owner.generation && pendingStarts.count > 0) {
|
|
2415
|
-
pendingComputerCallStarts.delete(owner.threadId);
|
|
2416
|
-
for (let i = 0; i < pendingStarts.count; i += 1)
|
|
2417
|
-
turnResources.beginComputerCall(resource, owner);
|
|
2418
|
-
}
|
|
2419
2285
|
}
|
|
2420
2286
|
function botForThread(botId, threadId) {
|
|
2421
2287
|
return directTurnBots.get(threadId) ?? store.projectBotForTask(botId, threadId) ?? store.bot(botId);
|
|
@@ -2461,15 +2327,9 @@ function canAdmitDirectTurn(botId, threadId) {
|
|
|
2461
2327
|
/** Routine and webhook dispatch shares startTurn's admission preconditions
|
|
2462
2328
|
* instead of waiting for whole-bot idleness: a free thread slot and no
|
|
2463
2329
|
* active group turn. A group turn blocks scheduled starts the same way it
|
|
2464
|
-
* blocks every other turn kind; it does not consume a capacity slot.
|
|
2465
|
-
*
|
|
2466
|
-
*
|
|
2467
|
-
* startTurn's cwd resolution: a free thread slot alone can miss a second
|
|
2468
|
-
* task of this bot landing in that one folder (#F-collide). Predicting that
|
|
2469
|
-
* collision here lets it defer through the same busy-target machinery as
|
|
2470
|
-
* capacity and group-turn contention, instead of reaching startTurn's own
|
|
2471
|
-
* workspace claim — which throws deep inside a detached dispatch that no
|
|
2472
|
-
* caller here awaits, so the run would otherwise fail permanently. */
|
|
2330
|
+
* blocks every other turn kind; it does not consume a capacity slot. The
|
|
2331
|
+
* bot's project folder is not a slot: a fresh run works there beside the
|
|
2332
|
+
* bot's other threads (see startTurn's cwd resolution). */
|
|
2473
2333
|
function unattendedDispatchState(botId) {
|
|
2474
2334
|
const bot = store.bot(botId);
|
|
2475
2335
|
const decision = admit("unattended", {}, {
|
|
@@ -2479,16 +2339,6 @@ function unattendedDispatchState(botId) {
|
|
|
2479
2339
|
});
|
|
2480
2340
|
if (decision.action === "refuse")
|
|
2481
2341
|
return decision.code === "missing" ? "missing" : "busy";
|
|
2482
|
-
if (bot?.cwd) {
|
|
2483
|
-
try {
|
|
2484
|
-
if (turnResources.blocker(workspaceResource(bot.cwd), { threadId: "", generation: "" }))
|
|
2485
|
-
return "busy";
|
|
2486
|
-
}
|
|
2487
|
-
catch {
|
|
2488
|
-
// An unresolvable folder is startTurn's own admission check to
|
|
2489
|
-
// report; this prediction only ever adds a defer, never a refusal.
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
2342
|
return "ready";
|
|
2493
2343
|
}
|
|
2494
2344
|
function requestedTaskBot(botId, rawThreadId) {
|
|
@@ -2839,8 +2689,12 @@ const routineRequestEnvelopeSchema = z.discriminatedUnion("action", [
|
|
|
2839
2689
|
]);
|
|
2840
2690
|
/** The loopback endpoint a bot's computer proxy polls before acting. With a
|
|
2841
2691
|
* Boat id, the same turn-scoped token also serves that Boat's tools at
|
|
2842
|
-
* /api/internal/computer/mcp, and is revoked when the turn ends.
|
|
2843
|
-
|
|
2692
|
+
* /api/internal/computer/mcp, and is revoked when the turn ends. This
|
|
2693
|
+
* computer runs as long as the app does, so its bridge keeps the thread's
|
|
2694
|
+
* bearer like every other integration; any other machine (a Local VM, the
|
|
2695
|
+
* VPS, a Boat) gets a new bearer each turn. */
|
|
2696
|
+
function controlIntegration(botId, threadId, generation, machine = {}) {
|
|
2697
|
+
const { localVmTarget, boxId } = machine === "this computer" ? {} : machine;
|
|
2844
2698
|
return {
|
|
2845
2699
|
url: `http://127.0.0.1:${PORT}/api/internal/computer-control?botId=${encodeURIComponent(botId)}`,
|
|
2846
2700
|
token: mintInternalCapability({
|
|
@@ -2855,7 +2709,7 @@ function controlIntegration(botId, threadId, generation, localVmTarget, boxId) {
|
|
|
2855
2709
|
skillAuthoring: false,
|
|
2856
2710
|
createdBots: 0,
|
|
2857
2711
|
openedThreads: 0,
|
|
2858
|
-
}),
|
|
2712
|
+
}, { perTurn: machine !== "this computer" }),
|
|
2859
2713
|
};
|
|
2860
2714
|
}
|
|
2861
2715
|
function askBotAndWait(targetBotId, message, depth, fromBotId, fromThreadId, targetThreadId) {
|
|
@@ -3153,9 +3007,6 @@ function packageImportDeps(selection) {
|
|
|
3153
3007
|
return avatarUrl;
|
|
3154
3008
|
},
|
|
3155
3009
|
},
|
|
3156
|
-
broadcast: (event) => event.kind === "bot"
|
|
3157
|
-
? broadcast({ kind: "bot", bot: publicBot(event.bot) })
|
|
3158
|
-
: broadcast({ kind: "group", group: publicGroupState(event.group) }),
|
|
3159
3010
|
defaultSelection: () => selection,
|
|
3160
3011
|
presets: presetStore,
|
|
3161
3012
|
};
|
|
@@ -3294,7 +3145,7 @@ const wireTask = (task) => {
|
|
|
3294
3145
|
return asleep ? { ...coordinated, snoozedUntil } : coordinated;
|
|
3295
3146
|
};
|
|
3296
3147
|
const wireBot = (bot) => {
|
|
3297
|
-
const { resumeCursors: _resumeCursors, tasks, approvalGrant, lastProfileRequestId: _lastProfileRequestId,
|
|
3148
|
+
const { resumeCursors: _resumeCursors, tasks, approvalGrant, lastProfileRequestId: _lastProfileRequestId, lastTeamSetupReceipt: _lastTeamSetupReceipt, packageBase: _packageBase, assignedSkills: _assignedSkills, ...rest } = bot;
|
|
3298
3149
|
// An elevated selection is inert until the desktop confirms its exact
|
|
3299
3150
|
// private reply. Every ordinary client sees the effective Ask state during
|
|
3300
3151
|
// that two-phase window, never a grant that may still roll back.
|
|
@@ -3316,7 +3167,7 @@ function toolScopeForTurn(botId) {
|
|
|
3316
3167
|
/** The correlated private response carries the requested value so Electron
|
|
3317
3168
|
* can validate it before sending the confirmation that makes it effective. */
|
|
3318
3169
|
const wireTrustedApprovalBot = (bot) => {
|
|
3319
|
-
const { resumeCursors: _resumeCursors, tasks, approvalGrant: _approvalGrant, lastProfileRequestId: _lastProfileRequestId,
|
|
3170
|
+
const { resumeCursors: _resumeCursors, tasks, approvalGrant: _approvalGrant, lastProfileRequestId: _lastProfileRequestId, lastTeamSetupReceipt: _lastTeamSetupReceipt, packageBase: _packageBase, assignedSkills: _assignedSkills, ...rest } = bot;
|
|
3320
3171
|
return { ...rest, approvalMode: approvalModeFor(rest), avatarUrl: rest.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map(wireTask) } : {}) };
|
|
3321
3172
|
};
|
|
3322
3173
|
/** A settings-based preview, not a receipt of a dispatched turn. No
|
|
@@ -3389,6 +3240,7 @@ function previewSystemPrompt(bot) {
|
|
|
3389
3240
|
{ id: "profile", label: "Profile changes", text: agentsMounted ? PROFILE_PROMPT : "" },
|
|
3390
3241
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
3391
3242
|
{ id: "team-memory", label: "Team memory", text: teamMemory.systemPrompt(bot.section) + (agentsMounted ? TEAM_MEMORY_PROMPT : "") },
|
|
3243
|
+
teamAvailabilityPart(agentsMounted && coordination ? peers : []),
|
|
3392
3244
|
{ id: "memory", label: "Memory", text: memorySystemPrompt(bot.id, { managedWrites: agentsMounted, fileTools: Boolean(privateWorkspace), enabled: bot.memoryEnabled !== false }) },
|
|
3393
3245
|
{ id: "skills", label: "Skills index", text: privateWorkspace ? skillsSystemPrompt(bot.id, skillsLibraryEnabled(cfg) ? bot.assignedSkills : undefined) : "" },
|
|
3394
3246
|
]);
|
|
@@ -3966,10 +3818,11 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
3966
3818
|
* fetched immediately and a deleted/guessed attachment id cannot become a
|
|
3967
3819
|
* dangling profile reference. */
|
|
3968
3820
|
const storedAvatarExists = (avatarUrl) => attachmentExists(avatarUrl.slice("/api/attachments/".length));
|
|
3821
|
+
/** A bot with its open thread's newest page, for HTTP replies and a
|
|
3822
|
+
* member stream's first frame of it. */
|
|
3969
3823
|
const publicBot = (bot) => ({
|
|
3970
3824
|
...wireBot(bot),
|
|
3971
|
-
|
|
3972
|
-
activeLeafId: store.activeLeaf(bot.threadId),
|
|
3825
|
+
...messagePage(bot.threadId, DEFAULT_PAGE),
|
|
3973
3826
|
tasks: store.tasks(bot.id).map(wireTask),
|
|
3974
3827
|
});
|
|
3975
3828
|
const publicBotQueuedMessages = () => queuedSteerSnapshot((botId, threadId) => Boolean(store.taskByThread(botId, threadId)));
|
|
@@ -4291,7 +4144,9 @@ function outstandingAssignmentsPrompt(threadId) {
|
|
|
4291
4144
|
requestId: node.id,
|
|
4292
4145
|
bot: store.bot(node.botId)?.name ?? "Teammate",
|
|
4293
4146
|
assignment: node.text.slice(0, 1_000),
|
|
4294
|
-
status: node.status === "queued" ? "waiting for that teammate to be free"
|
|
4147
|
+
status: node.status === "queued" ? "waiting for that teammate to be free"
|
|
4148
|
+
: openPersonCard(node.threadId) ? "stopped on a card waiting for the person's approval or answer in that teammate's thread; tell the person to open it"
|
|
4149
|
+
: "working on it now",
|
|
4295
4150
|
}));
|
|
4296
4151
|
return ` Assignments you already sent are still outstanding, and nothing in this conversation cancelled them: ${JSON.stringify(listed)}. Do not resend them, do not poll or wait for them, and do not tell the user they were lost. Each result returns to this conversation on its own and resumes you then. To change or add to one, send the change to the same teammate with coordinate_bots; it runs after the current work. Answer the message above with that work still in flight.`;
|
|
4297
4152
|
}
|
|
@@ -4804,18 +4659,33 @@ function activeGroupTurnForBot(botId) {
|
|
|
4804
4659
|
}
|
|
4805
4660
|
return null;
|
|
4806
4661
|
}
|
|
4662
|
+
/** A room with its open thread's newest page, for HTTP replies and a
|
|
4663
|
+
* member stream's first frame of it. */
|
|
4807
4664
|
const groupWithThread = (group) => ({
|
|
4808
4665
|
...publicGroupState(group),
|
|
4809
|
-
|
|
4810
|
-
activeLeafId: store.activeLeaf(group.threadId),
|
|
4666
|
+
...messagePage(group.threadId, DEFAULT_PAGE),
|
|
4811
4667
|
...(group.dm ? {} : { tasks: store.groupTasks(group.id) }),
|
|
4812
4668
|
});
|
|
4813
4669
|
// The store tells us what it wrote; this is the ONE place that turns those
|
|
4814
4670
|
// into SSE frames. No mutation path can persist without emitting — the
|
|
4815
4671
|
// property holds by construction, not by every call site remembering to
|
|
4816
|
-
// broadcast.
|
|
4817
|
-
//
|
|
4818
|
-
//
|
|
4672
|
+
// broadcast.
|
|
4673
|
+
//
|
|
4674
|
+
// A bot or room frame carries its open thread's newest page only when that
|
|
4675
|
+
// thread is not the one it last went out with: a switch, a new open thread,
|
|
4676
|
+
// deleting the open one, a new bot or room. Other windows swap transcripts
|
|
4677
|
+
// on exactly those frames (one whose open thread was deleted keeps its
|
|
4678
|
+
// composer locked until a page arrives). Every other frame stays slim, and
|
|
4679
|
+
// clients keep the transcript they hold when a frame carries none.
|
|
4680
|
+
const sentThreads = new Map();
|
|
4681
|
+
for (const record of [...store.bots, ...store.groups])
|
|
4682
|
+
sentThreads.set(record.id, record.threadId);
|
|
4683
|
+
function pageIfThreadChanged(id, threadId) {
|
|
4684
|
+
if (sentThreads.get(id) === threadId)
|
|
4685
|
+
return {};
|
|
4686
|
+
sentThreads.set(id, threadId);
|
|
4687
|
+
return messagePage(threadId, DEFAULT_PAGE);
|
|
4688
|
+
}
|
|
4819
4689
|
store.onChange((change) => {
|
|
4820
4690
|
// Who owns which thread, and what each member may see, follow the fleet.
|
|
4821
4691
|
if (change.type !== "message" && change.type !== "message.patch" && change.type !== "thread" && change.type !== "sections")
|
|
@@ -4824,11 +4694,12 @@ store.onChange((change) => {
|
|
|
4824
4694
|
case "sections":
|
|
4825
4695
|
broadcast({ kind: "sections", sections: store.sections });
|
|
4826
4696
|
break;
|
|
4697
|
+
// Live frames never carry screenshot pixels; clients load them by URL.
|
|
4827
4698
|
case "message":
|
|
4828
|
-
broadcast({ kind: "message", threadId: change.threadId, message: change.message });
|
|
4699
|
+
broadcast({ kind: "message", threadId: change.threadId, message: slimMessage(change.message) });
|
|
4829
4700
|
break;
|
|
4830
4701
|
case "message.patch":
|
|
4831
|
-
broadcast({ kind: "message.patch", threadId: change.threadId, message: change.message });
|
|
4702
|
+
broadcast({ kind: "message.patch", threadId: change.threadId, message: slimMessage(change.message) });
|
|
4832
4703
|
break;
|
|
4833
4704
|
case "thread":
|
|
4834
4705
|
broadcast({ kind: "thread", threadId: change.threadId, activeLeafId: change.activeLeafId });
|
|
@@ -4848,7 +4719,7 @@ store.onChange((change) => {
|
|
|
4848
4719
|
case "bot": {
|
|
4849
4720
|
const bot = store.bot(change.botId);
|
|
4850
4721
|
if (bot)
|
|
4851
|
-
broadcast({ kind: "bot", bot: wireBot(bot) });
|
|
4722
|
+
broadcast({ kind: "bot", bot: { ...wireBot(bot), ...pageIfThreadChanged(bot.id, bot.threadId) } });
|
|
4852
4723
|
// A new audience narrows the rooms this bot is in, for good.
|
|
4853
4724
|
if (bot)
|
|
4854
4725
|
for (const group of store.groups.filter((candidate) => candidate.memberIds.includes(bot.id)))
|
|
@@ -4856,6 +4727,7 @@ store.onChange((change) => {
|
|
|
4856
4727
|
break;
|
|
4857
4728
|
}
|
|
4858
4729
|
case "bot.deleted":
|
|
4730
|
+
sentThreads.delete(change.botId);
|
|
4859
4731
|
try {
|
|
4860
4732
|
commandAllowlist.clear(change.botId);
|
|
4861
4733
|
}
|
|
@@ -4867,12 +4739,13 @@ store.onChange((change) => {
|
|
|
4867
4739
|
case "group": {
|
|
4868
4740
|
const group = store.group(change.groupId);
|
|
4869
4741
|
if (group)
|
|
4870
|
-
broadcast({ kind: "group", group: publicGroupState(group) });
|
|
4742
|
+
broadcast({ kind: "group", group: { ...publicGroupState(group), ...pageIfThreadChanged(group.id, group.threadId) } });
|
|
4871
4743
|
if (group)
|
|
4872
4744
|
settleRoomFloor(group);
|
|
4873
4745
|
break;
|
|
4874
4746
|
}
|
|
4875
4747
|
case "group.deleted":
|
|
4748
|
+
sentThreads.delete(change.groupId);
|
|
4876
4749
|
broadcast({ kind: "group.deleted", groupId: change.groupId });
|
|
4877
4750
|
break;
|
|
4878
4751
|
}
|
|
@@ -4897,12 +4770,13 @@ function pageSize(raw) {
|
|
|
4897
4770
|
return null;
|
|
4898
4771
|
return Math.min(size, MESSAGE_PAGE_MAX);
|
|
4899
4772
|
}
|
|
4900
|
-
/** A screen message without its pixels
|
|
4901
|
-
* `/api/threads/:threadId/messages/:id/image`
|
|
4773
|
+
/** A screen message without its pixels, for live frames and pages alike.
|
|
4774
|
+
* The client fetches those from `/api/threads/:threadId/messages/:id/image`
|
|
4775
|
+
* when it actually shows one; `mime` stays so it can name a download. */
|
|
4902
4776
|
function slimMessage(message) {
|
|
4903
4777
|
if (message.kind !== "screen" || !message.png)
|
|
4904
4778
|
return message;
|
|
4905
|
-
const { png: _png,
|
|
4779
|
+
const { png: _png, ...rest } = message;
|
|
4906
4780
|
return { ...rest, hasImage: true };
|
|
4907
4781
|
}
|
|
4908
4782
|
/** `limit === undefined` is the original, unpaginated shape. A bounded,
|
|
@@ -5309,7 +5183,7 @@ always, rememberCommand) {
|
|
|
5309
5183
|
}
|
|
5310
5184
|
/** A decision or answer on a provider/peer card, made on behalf of `auth`
|
|
5311
5185
|
* (a Live call answers as the person who started the call). Harness-native
|
|
5312
|
-
* proposals (skill, routine, profile, default model,
|
|
5186
|
+
* proposals (skill, routine, profile, default model, team setup)
|
|
5313
5187
|
* are not handled here; they are reviewed on screen (liveDecisionRefusal).
|
|
5314
5188
|
* Mirrors POST /api/threads/:id/respond. */
|
|
5315
5189
|
async function respondToCard(input) {
|
|
@@ -5468,10 +5342,7 @@ const watchdog = new TurnWatchdog({
|
|
|
5468
5342
|
repeats.settle(turn.threadId);
|
|
5469
5343
|
const bot = botForThread(turn.botId, turn.threadId);
|
|
5470
5344
|
const routineRun = activeRoutineRunForThread(turn.threadId);
|
|
5471
|
-
|
|
5472
|
-
? runningTurnInstance(bot, turn.threadId)
|
|
5473
|
-
: runningTurnEngines.get(turn.threadId) ?? null;
|
|
5474
|
-
void instance?.adapter.interruptTurn(turn.threadId).catch(() => { });
|
|
5345
|
+
void runningTurnInstance(bot, turn.threadId)?.adapter.interruptTurn(turn.threadId).catch(() => { });
|
|
5475
5346
|
const minutes = Math.round(TURN_STALL_MS / 60_000);
|
|
5476
5347
|
if (routineRun?.target === "bot") {
|
|
5477
5348
|
routines?.failThread(turn.threadId, `No activity for ${minutes} minutes — the routine was stopped`);
|
|
@@ -5481,7 +5352,7 @@ const watchdog = new TurnWatchdog({
|
|
|
5481
5352
|
store.appendMessage(turn.threadId, {
|
|
5482
5353
|
role: "bot",
|
|
5483
5354
|
kind: "activity",
|
|
5484
|
-
tool:
|
|
5355
|
+
tool: failedTurnTool(`no activity for ${minutes} minutes — the turn was stopped`),
|
|
5485
5356
|
});
|
|
5486
5357
|
// a routine's stall reports through its own failure path
|
|
5487
5358
|
if (bot && routineRun?.target !== "bot") {
|
|
@@ -5761,6 +5632,8 @@ bus.subscribe((event) => {
|
|
|
5761
5632
|
});
|
|
5762
5633
|
// Per-thread state the digest needs from before the turn: when it started,
|
|
5763
5634
|
// and the checkpoint taken at dispatch (hash + folder) so settle can diff.
|
|
5635
|
+
// `pin` is the dispatch that pinned that commit in the shadow repo, so a
|
|
5636
|
+
// sibling thread's snapshot in the same folder cannot reclaim it mid-turn.
|
|
5764
5637
|
const turnStartedAt = new Map();
|
|
5765
5638
|
const turnCheckpoints = new Map();
|
|
5766
5639
|
// The turn a thread is in, from the last event that named one. Not every
|
|
@@ -5768,9 +5641,18 @@ const turnCheckpoints = new Map();
|
|
|
5768
5641
|
// a digest can only count activity it can attribute — so an unstamped tool
|
|
5769
5642
|
// row is attributed to the thread's live turn instead of to nothing.
|
|
5770
5643
|
const liveTurnByThread = new Map();
|
|
5644
|
+
/** Forget a thread's pre-turn checkpoint and drop its pin, so the shadow
|
|
5645
|
+
* repo's next cleanup may reclaim that commit. */
|
|
5646
|
+
function forgetTurnCheckpoint(threadId) {
|
|
5647
|
+
const checkpoint = turnCheckpoints.get(threadId);
|
|
5648
|
+
if (!checkpoint)
|
|
5649
|
+
return;
|
|
5650
|
+
turnCheckpoints.delete(threadId);
|
|
5651
|
+
void checkpoints.release(checkpoint.botId, checkpoint.cwd, checkpoint.pin);
|
|
5652
|
+
}
|
|
5771
5653
|
function clearTurnDigestState(threadId) {
|
|
5772
5654
|
turnStartedAt.delete(threadId);
|
|
5773
|
-
|
|
5655
|
+
forgetTurnCheckpoint(threadId);
|
|
5774
5656
|
liveTurnByThread.delete(threadId);
|
|
5775
5657
|
memoryRowsByThread.delete(threadId);
|
|
5776
5658
|
}
|
|
@@ -5839,9 +5721,8 @@ function ingestEngineHook(capability, body) {
|
|
|
5839
5721
|
}
|
|
5840
5722
|
return { ok: true };
|
|
5841
5723
|
}
|
|
5842
|
-
/** Persist observed tools and memory immediately
|
|
5843
|
-
*
|
|
5844
|
-
* that same row only if its task and active branch still exist. */
|
|
5724
|
+
/** Persist observed tools and memory immediately, then enrich that same
|
|
5725
|
+
* row only if its task and active branch still exist. */
|
|
5845
5726
|
async function scheduleTurnDigest(input) {
|
|
5846
5727
|
const at = Date.now();
|
|
5847
5728
|
const startedAt = turnStartedAt.get(input.threadId);
|
|
@@ -5886,6 +5767,8 @@ async function scheduleTurnDigest(input) {
|
|
|
5886
5767
|
}
|
|
5887
5768
|
if (!checkpoint)
|
|
5888
5769
|
return;
|
|
5770
|
+
// A folder-level diff: when a sibling thread of this bot works in the
|
|
5771
|
+
// same folder, its edits since this turn's snapshot are listed here too.
|
|
5889
5772
|
const controller = new AbortController();
|
|
5890
5773
|
let timer;
|
|
5891
5774
|
const capture = async () => {
|
|
@@ -5909,6 +5792,12 @@ async function scheduleTurnDigest(input) {
|
|
|
5909
5792
|
catch (error) {
|
|
5910
5793
|
console.error(`digest: could not record turn ${input.turnId} on ${input.threadId}:`, error instanceof Error ? error.message : error);
|
|
5911
5794
|
}
|
|
5795
|
+
finally {
|
|
5796
|
+
// The pre-turn commit has had its last reader (the diff above is queued
|
|
5797
|
+
// ahead of this in the shadow repo); the next snapshot may reclaim it.
|
|
5798
|
+
if (checkpoint)
|
|
5799
|
+
void checkpoints.release(checkpoint.botId, checkpoint.cwd, checkpoint.pin);
|
|
5800
|
+
}
|
|
5912
5801
|
}
|
|
5913
5802
|
// Bots currently working with nobody at the keyboard — a webhook turn, or a
|
|
5914
5803
|
// turn a webhook-driven bot handed to a teammate. Auto mode is a decision
|
|
@@ -6044,9 +5933,6 @@ const vpsJoinRequests = new Map();
|
|
|
6044
5933
|
const orphanBoatLifecycleBusyIds = new Set();
|
|
6045
5934
|
const boatInventoryRequestsBusyIds = new Set();
|
|
6046
5935
|
const computerProviderConfigTransitions = new Set();
|
|
6047
|
-
// A restore mutates and cleans a project work tree. Claim the bot across the
|
|
6048
|
-
// entire async Git operation so a turn cannot start in that folder midway.
|
|
6049
|
-
const checkpointRestoreLeases = new Set();
|
|
6050
5936
|
/** The effective idle window, read from config so a Settings change applies
|
|
6051
5937
|
* to both new and already-armed timers (see the config route). */
|
|
6052
5938
|
const localVmIdleMs = () => localVmIdleTimeoutMinutes(cfg) * 60_000;
|
|
@@ -6111,10 +5997,6 @@ async function computerCallGate(internalCapability) {
|
|
|
6111
5997
|
};
|
|
6112
5998
|
}
|
|
6113
5999
|
const computer = turnComputerResources.get(internalCapability.threadId);
|
|
6114
|
-
// The screen call arriving here re-claims an idle-released seat directly
|
|
6115
|
-
// (#1653): inside the reclaim window the previous holder picks its seat
|
|
6116
|
-
// back up without re-entering the wait, and the successful claim
|
|
6117
|
-
// restarts the quiet window. A seat another turn holds still refuses.
|
|
6118
6000
|
if (!snapshot.held && computer && computer.owner.generation === internalCapability.generation &&
|
|
6119
6001
|
!claimTurnResource(computer.owner, computer.resource)) {
|
|
6120
6002
|
return {
|
|
@@ -6194,10 +6076,10 @@ async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
|
|
|
6194
6076
|
if (!machine)
|
|
6195
6077
|
throw new Error("The team's Boat computer is missing; explicitly create or retry it from the Team map");
|
|
6196
6078
|
await bindTurnComputer(owner, `computer:box:${machine.id}`, true);
|
|
6197
|
-
const action = boat.boatTurnLifecycleAction({ explicitCloud: true,
|
|
6079
|
+
const action = boat.boatTurnLifecycleAction({ explicitCloud: true, state: typeof machine.state === "string" ? machine.state : null });
|
|
6198
6080
|
if (action === "wake")
|
|
6199
6081
|
machine = await boat.readyBoat(cfg, ownerId);
|
|
6200
|
-
if (!machine || boat.boatTurnLifecycleAction({ explicitCloud: true,
|
|
6082
|
+
if (!machine || boat.boatTurnLifecycleAction({ explicitCloud: true, state: typeof machine.state === "string" ? machine.state : null }) !== "attach") {
|
|
6201
6083
|
throw new Error("The team computer is not ready; check it in the Team map");
|
|
6202
6084
|
}
|
|
6203
6085
|
if (turnResourceOwners.get(owner.threadId)?.generation !== owner.generation ||
|
|
@@ -6208,6 +6090,10 @@ async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
|
|
|
6208
6090
|
capture: () => boat.screenshotBoat(cfg, ownerId, machine.id),
|
|
6209
6091
|
};
|
|
6210
6092
|
}
|
|
6093
|
+
/** The two engine facts the cloud-computer rule reads (shared/cloud-computer.ts). */
|
|
6094
|
+
function cloudEngine(instance) {
|
|
6095
|
+
return { driverKind: instance?.driverKind, computerMcp: instance?.adapter.capabilities.computerMcp };
|
|
6096
|
+
}
|
|
6211
6097
|
/** How a turn's engine reaches a cloud computer. The Computer engine runs its
|
|
6212
6098
|
* whole turn on the Boat. Every other engine keeps its own model and sign-in
|
|
6213
6099
|
* and gets the Boat as one more stdio computer server (harness-mcp-proxy
|
|
@@ -6217,7 +6103,7 @@ async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
|
|
|
6217
6103
|
function cloudComputerMount(botId, owner, boxId, remoteAgent) {
|
|
6218
6104
|
if (remoteAgent)
|
|
6219
6105
|
return { computer: { kind: "box", boxId } };
|
|
6220
|
-
const control = controlIntegration(botId, owner.threadId, owner.generation,
|
|
6106
|
+
const control = controlIntegration(botId, owner.threadId, owner.generation, { boxId });
|
|
6221
6107
|
return {
|
|
6222
6108
|
localComputer: {
|
|
6223
6109
|
command: process.execPath,
|
|
@@ -6245,7 +6131,7 @@ async function mountHostComputer(owner, botId, providerSupportsLocal) {
|
|
|
6245
6131
|
: "CUA Driver is not ready for this computer — check permissions and restart OpenMausBot");
|
|
6246
6132
|
}
|
|
6247
6133
|
await bindTurnComputer(owner, "computer:host");
|
|
6248
|
-
return gatedLocalComputer(cua, controlIntegration(botId, owner.threadId, owner.generation));
|
|
6134
|
+
return gatedLocalComputer(cua, controlIntegration(botId, owner.threadId, owner.generation, "this computer"));
|
|
6249
6135
|
}
|
|
6250
6136
|
/** The bot's own VPS desktop, mounted into the local agent. The desktop lease
|
|
6251
6137
|
* is claimed on the first computer call, through the computer-control gate
|
|
@@ -6285,7 +6171,9 @@ async function mountBotVps(bot, owner, opts) {
|
|
|
6285
6171
|
}
|
|
6286
6172
|
/** The bot's own Boat. Explicit Cloud is the consent boundary: it may create a
|
|
6287
6173
|
* missing machine and wake an archived one (~8s, and it un-pauses billing);
|
|
6288
|
-
* Auto only
|
|
6174
|
+
* Auto (only ever the Computer engine, see startTurn) only attaches a machine
|
|
6175
|
+
* that is already running. Callers have already checked that the engine can
|
|
6176
|
+
* work on it (cloudPlaceDriverError). */
|
|
6289
6177
|
async function attachBotBoat(bot, owner, opts) {
|
|
6290
6178
|
// Explicit cloud turns can provision/wake the same bot's Boat. Claim before
|
|
6291
6179
|
// any network await so setup itself cannot race another turn.
|
|
@@ -6304,7 +6192,6 @@ async function attachBotBoat(bot, owner, opts) {
|
|
|
6304
6192
|
}
|
|
6305
6193
|
const lifecycleOf = (explicitCloud) => boat.boatTurnLifecycleAction({
|
|
6306
6194
|
explicitCloud,
|
|
6307
|
-
canMount: opts.canMount,
|
|
6308
6195
|
state: typeof b?.state === "string" ? b.state : null,
|
|
6309
6196
|
});
|
|
6310
6197
|
let lifecycle = lifecycleOf(opts.explicitCloud);
|
|
@@ -6328,7 +6215,7 @@ async function attachBotBoat(bot, owner, opts) {
|
|
|
6328
6215
|
await bindTurnComputer(owner, `computer:box:${machine.id}`, opts.remoteAgent);
|
|
6329
6216
|
return {
|
|
6330
6217
|
capture: () => boat.screenshotBoat(cfg, bot.id, machine.id),
|
|
6331
|
-
mount:
|
|
6218
|
+
mount: cloudComputerMount(bot.id, owner, machine.id, opts.remoteAgent),
|
|
6332
6219
|
};
|
|
6333
6220
|
}
|
|
6334
6221
|
function managedBoatOwners() {
|
|
@@ -6447,7 +6334,8 @@ async function computerPreviewSurface(bot, threadId) {
|
|
|
6447
6334
|
* A Cloud home lists only the places it has (cloud-home.ts), so a bot never
|
|
6448
6335
|
* tells the person to set up this computer or a Local VM there. */
|
|
6449
6336
|
async function selectableComputers(bot) {
|
|
6450
|
-
const
|
|
6337
|
+
const instance = registry.get(bot.modelSelection.instanceId);
|
|
6338
|
+
const caps = instance?.adapter.capabilities;
|
|
6451
6339
|
const off = bot.computer === "off";
|
|
6452
6340
|
const localEngine = caps?.remoteAgent !== true;
|
|
6453
6341
|
const surfaces = ["cloud", "vm", "local", "browser"].filter(surface => !CLOUD_HOME || cloudHomeOffersPlace(surface));
|
|
@@ -6465,16 +6353,16 @@ async function selectableComputers(bot) {
|
|
|
6465
6353
|
reason = "Computer access is Off in this bot's settings.";
|
|
6466
6354
|
else if (surface === "cloud") {
|
|
6467
6355
|
if (bot.cloudBackend === "vps") {
|
|
6468
|
-
const status =
|
|
6356
|
+
const status = canWorkOnCloud(cloudEngine(instance), "vps") ? await vps.vpsComputerStatus(cfg, bot.id) : null;
|
|
6469
6357
|
ready = status?.ready === true;
|
|
6470
6358
|
canStart = Boolean(status?.daemonUp && status.managed && status.container === "stopped" &&
|
|
6471
6359
|
status.image && status.imageMatches && status.network === "private" && status.mounts === "none" && status.security === "hardened");
|
|
6472
6360
|
canCreate = Boolean(status?.configured && status.daemonUp && status.container === "missing");
|
|
6473
6361
|
reason = status?.problem ?? reason;
|
|
6474
6362
|
}
|
|
6475
|
-
else if (boat.boatConfigured(cfg) &&
|
|
6363
|
+
else if (boat.boatConfigured(cfg) && canWorkOnCloud(cloudEngine(instance), "box")) {
|
|
6476
6364
|
const status = await boat.boatStatus(cfg, bot.id);
|
|
6477
|
-
const lifecycle = boat.boatTurnLifecycleAction({ explicitCloud: true,
|
|
6365
|
+
const lifecycle = boat.boatTurnLifecycleAction({ explicitCloud: true, state: status.box?.state ?? null });
|
|
6478
6366
|
ready = lifecycle === "attach";
|
|
6479
6367
|
canStart = lifecycle === "wake";
|
|
6480
6368
|
canCreate = lifecycle === "provision";
|
|
@@ -6553,7 +6441,7 @@ function continueComputerSelection(threadId, generation, succeeded) {
|
|
|
6553
6441
|
const runningTurnEngines = new Map();
|
|
6554
6442
|
function runningTurnInstance(bot, threadId) {
|
|
6555
6443
|
// A turn always runs on the bot's own engine, wherever it works.
|
|
6556
|
-
return runningTurnEngines.get(threadId) ?? registry.get(bot.modelSelection.instanceId);
|
|
6444
|
+
return runningTurnEngines.get(threadId) ?? (bot ? registry.get(bot.modelSelection.instanceId) : null);
|
|
6557
6445
|
}
|
|
6558
6446
|
function providerTransitionForTurn(bot, runOn, threadId) {
|
|
6559
6447
|
const provider = turnProvider(bot, runOn, threadId);
|
|
@@ -6934,38 +6822,6 @@ bus.subscribe((event) => {
|
|
|
6934
6822
|
if (touches || /computer|screenshot|click|type_text|press_key|scroll|open_url|wait_for|browser_/i.test(toolName)) {
|
|
6935
6823
|
pokeScreenPoller(event.threadId, touches, surface);
|
|
6936
6824
|
}
|
|
6937
|
-
// A completed screen-touching computer tool is real screen
|
|
6938
|
-
// activity (#1653): it restarts the idle clock on that turn's
|
|
6939
|
-
// computer claim. The poller's own frames never arrive here, so
|
|
6940
|
-
// they cannot keep a quiet seat held.
|
|
6941
|
-
if (touches && surface === "computer") {
|
|
6942
|
-
const computer = turnComputerResources.get(event.threadId);
|
|
6943
|
-
if (computer) {
|
|
6944
|
-
// A quiet window cannot expire while this call executes.
|
|
6945
|
-
turnResources.endComputerCall(computer.resource, computer.owner);
|
|
6946
|
-
turnResources.activity(computer.resource, computer.owner);
|
|
6947
|
-
}
|
|
6948
|
-
else {
|
|
6949
|
-
// The call never forwarded onto a bound desktop, or the lazy
|
|
6950
|
-
// bind never landed: drop its pending start so it cannot
|
|
6951
|
-
// fence a claim taken later (#1653).
|
|
6952
|
-
const pending = pendingComputerCallStarts.get(event.threadId);
|
|
6953
|
-
if (pending) {
|
|
6954
|
-
if (pending.count > 1)
|
|
6955
|
-
pendingComputerCallStarts.set(event.threadId, { ...pending, count: pending.count - 1 });
|
|
6956
|
-
else
|
|
6957
|
-
pendingComputerCallStarts.delete(event.threadId);
|
|
6958
|
-
}
|
|
6959
|
-
}
|
|
6960
|
-
// Real cloud-screen work refreshes the #1655 idle stop for
|
|
6961
|
-
// that seat; the poller's own frames never arrive here, so
|
|
6962
|
-
// preview traffic cannot keep a paid machine awake.
|
|
6963
|
-
if (computer?.resource.startsWith("computer:box:")) {
|
|
6964
|
-
const seatBot = store.botByThread(event.threadId);
|
|
6965
|
-
if (seatBot)
|
|
6966
|
-
cloudSeatLeases.get(seatBot.id)?.touch();
|
|
6967
|
-
}
|
|
6968
|
-
}
|
|
6969
6825
|
}
|
|
6970
6826
|
}
|
|
6971
6827
|
break;
|
|
@@ -6988,26 +6844,6 @@ bus.subscribe((event) => {
|
|
|
6988
6844
|
});
|
|
6989
6845
|
if (event.itemId)
|
|
6990
6846
|
toolMessageByItem.set(`${event.threadId}:${event.itemId}`, message.id);
|
|
6991
|
-
// A screen-touching computer tool opening is an in-flight call on
|
|
6992
|
-
// that turn's claim (#1653): the seat cannot be idle-released out
|
|
6993
|
-
// from under I/O the desktop is still executing. Its completion —
|
|
6994
|
-
// or the turn settling — drops the fence.
|
|
6995
|
-
if (screenTouchingTool(name) && screenSurfaceForTool(name) === "computer") {
|
|
6996
|
-
const computer = turnComputerResources.get(event.threadId);
|
|
6997
|
-
if (computer) {
|
|
6998
|
-
turnResources.beginComputerCall(computer.resource, computer.owner);
|
|
6999
|
-
}
|
|
7000
|
-
else {
|
|
7001
|
-
// A lazy attach (#1361) may not have seated the claim yet: the
|
|
7002
|
-
// gate finishes the bind before the call forwards, so hold the
|
|
7003
|
-
// start here and let bindTurnComputer fence the fresh claim.
|
|
7004
|
-
const generation = activeInternalGenerationByThread.get(event.threadId);
|
|
7005
|
-
if (generation) {
|
|
7006
|
-
const pending = pendingComputerCallStarts.get(event.threadId);
|
|
7007
|
-
pendingComputerCallStarts.set(event.threadId, pending?.generation === generation ? { generation, count: pending.count + 1 } : { generation, count: 1 });
|
|
7008
|
-
}
|
|
7009
|
-
}
|
|
7010
|
-
}
|
|
7011
6847
|
}
|
|
7012
6848
|
break;
|
|
7013
6849
|
case "request.opened": {
|
|
@@ -7019,8 +6855,9 @@ bus.subscribe((event) => {
|
|
|
7019
6855
|
// A permission request here is one the provider left for a person: its
|
|
7020
6856
|
// own mode already ran (Ask, Edits, Auto's reviewer, Custom's config).
|
|
7021
6857
|
// Only the person's explicit Full access or exact saved command answers.
|
|
7022
|
-
// The app does not guess whether
|
|
7023
|
-
// always reaches the human — even Full
|
|
6858
|
+
// Approve for me allows a web search. The app does not guess whether
|
|
6859
|
+
// the action is safe. A QUESTION always reaches the human — even Full
|
|
6860
|
+
// access never invents an answer.
|
|
7024
6861
|
const asker = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
7025
6862
|
const unattended = permission && asker && event.requestId ? isUnattended(asker.id, event.threadId) : false;
|
|
7026
6863
|
// A turn a guest drives on a Cloud home is Ask, room turns included,
|
|
@@ -7082,9 +6919,9 @@ bus.subscribe((event) => {
|
|
|
7082
6919
|
if (outcome !== "unavailable")
|
|
7083
6920
|
pushMessage({
|
|
7084
6921
|
role: "bot", kind: "activity",
|
|
7085
|
-
tool:
|
|
7086
|
-
|
|
7087
|
-
|
|
6922
|
+
tool: outcome === "rejected"
|
|
6923
|
+
? { name: `The provider rejected this action despite ${verdict.source === "command-allowlist" ? "the saved command permission" : "Full access"}.`, ok: false }
|
|
6924
|
+
: failedTurnTool("could not deliver approval to the provider; retry the task after reconnecting."),
|
|
7088
6925
|
});
|
|
7089
6926
|
return;
|
|
7090
6927
|
}
|
|
@@ -7176,6 +7013,8 @@ bus.subscribe((event) => {
|
|
|
7176
7013
|
store.setTaskActivity(bot.id, event.threadId, "waiting-on-you");
|
|
7177
7014
|
else if (asker.busy)
|
|
7178
7015
|
store.setActivity(asker.id, "waiting-on-you");
|
|
7016
|
+
if (event.requestId)
|
|
7017
|
+
showWaitingOnPersonChip(event.threadId, event.requestId, asker, permission ? "approval" : "answer");
|
|
7179
7018
|
const notificationBot = (routineRun && routineSourceOwner(routineRun)?.bot) || asker;
|
|
7180
7019
|
notify(buildNotification(permission ? "approval" : "question", notificationBot, (routineRun && routineSourceThread(routineRun)) || event.threadId, event.summary));
|
|
7181
7020
|
}
|
|
@@ -7185,6 +7024,8 @@ bus.subscribe((event) => {
|
|
|
7185
7024
|
case "request.resolved": {
|
|
7186
7025
|
if (event.requestId)
|
|
7187
7026
|
pendingCommandRules.delete(`${event.threadId}:${event.requestId}`);
|
|
7027
|
+
if (event.requestId)
|
|
7028
|
+
settleWaitingOnPersonChips(event.threadId, event.requestId);
|
|
7188
7029
|
// answered (by whoever): the turn is working again, unless it settled
|
|
7189
7030
|
const waiting = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
7190
7031
|
if (bot && store.taskByThread(bot.id, event.threadId)?.activity === "waiting-on-you") {
|
|
@@ -7230,14 +7071,7 @@ bus.subscribe((event) => {
|
|
|
7230
7071
|
pushMessage({
|
|
7231
7072
|
role: "bot",
|
|
7232
7073
|
kind: "activity",
|
|
7233
|
-
tool:
|
|
7234
|
-
// a setup error carries its fix, which must not be cut mid-command
|
|
7235
|
-
name: `error: ${event.message.slice(0, event.setup ? 320 : 160)}`,
|
|
7236
|
-
ok: false,
|
|
7237
|
-
setup: event.setup,
|
|
7238
|
-
...(event.terminal ? { terminal: true } : {}),
|
|
7239
|
-
...(event.claudeUpdate ? { claudeUpdate: true } : {}),
|
|
7240
|
-
},
|
|
7074
|
+
tool: failedTurnTool(event.message, event),
|
|
7241
7075
|
});
|
|
7242
7076
|
// a setup error means the engine could not even start: the bot is
|
|
7243
7077
|
// dead until something changes, not merely idle. The next successful
|
|
@@ -7255,6 +7089,7 @@ bus.subscribe((event) => {
|
|
|
7255
7089
|
}
|
|
7256
7090
|
break;
|
|
7257
7091
|
case "turn.completed": {
|
|
7092
|
+
settleWaitingOnPersonChips(event.threadId);
|
|
7258
7093
|
// A peer-started turn settles as coordination, not as news. What keeps
|
|
7259
7094
|
// that classification from outliving its turn is the rewrite at
|
|
7260
7095
|
// dispatch, not this line — releasing it here too is hygiene, so a
|
|
@@ -7472,8 +7307,8 @@ bus.subscribe((event) => {
|
|
|
7472
7307
|
// still be in flight).
|
|
7473
7308
|
//
|
|
7474
7309
|
// Keep the thread busy until its bounded final capture releases
|
|
7475
|
-
// the screen
|
|
7476
|
-
//
|
|
7310
|
+
// the screen: an accepted follow-up must not take the desktop
|
|
7311
|
+
// before the END-state frame is captured.
|
|
7477
7312
|
const settleLeafId = store.activePath(event.threadId).at(-1)?.id;
|
|
7478
7313
|
let timeout;
|
|
7479
7314
|
const screenSettled = Promise.race([
|
|
@@ -7565,6 +7400,57 @@ bus.subscribe((event) => {
|
|
|
7565
7400
|
* channel that only ever shows requests is half a record. Mirror the
|
|
7566
7401
|
* reply on success; mirror a failed/stopped terminal chip otherwise. */
|
|
7567
7402
|
const delegationWatch = new Map();
|
|
7403
|
+
/** The card in this thread that is waiting on the person: an approval, a
|
|
7404
|
+
* question or a proposal to review, newest first. */
|
|
7405
|
+
function openPersonCard(threadId) {
|
|
7406
|
+
const messages = store.messagesFor(threadId);
|
|
7407
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
7408
|
+
const card = messages[i].kind === "options" ? messages[i].card : undefined;
|
|
7409
|
+
const kind = liveCardKind(card);
|
|
7410
|
+
if (kind)
|
|
7411
|
+
return { kind, tool: card?.tool };
|
|
7412
|
+
}
|
|
7413
|
+
return undefined;
|
|
7414
|
+
}
|
|
7415
|
+
/** The conversation waiting on work that runs in this thread: a Chief's
|
|
7416
|
+
* delegate_bot task or coordinate_bots assignment. */
|
|
7417
|
+
function delegatorOf(threadId) {
|
|
7418
|
+
const watch = delegationWatch.get(threadId);
|
|
7419
|
+
if (watch?.sourceThreadId)
|
|
7420
|
+
return { threadId: watch.sourceThreadId, botId: watch.sourceBotId };
|
|
7421
|
+
const parent = roomHandoffs.assignerOf(threadId);
|
|
7422
|
+
return parent ? { threadId: parent.threadId, botId: parent.botId } : undefined;
|
|
7423
|
+
}
|
|
7424
|
+
// MOCA-274: a delegated teammate's card used to live only in its own thread,
|
|
7425
|
+
// so the person talking to the Chief never saw that work was waiting on them.
|
|
7426
|
+
// A chip in the delegating conversation points at the card until it settles.
|
|
7427
|
+
const waitingOnPersonChips = new Map();
|
|
7428
|
+
function showWaitingOnPersonChip(threadId, requestId, asker, kind) {
|
|
7429
|
+
const delegator = delegatorOf(threadId);
|
|
7430
|
+
if (!delegator || delegator.threadId === threadId)
|
|
7431
|
+
return;
|
|
7432
|
+
const chip = store.appendMessage(delegator.threadId, { role: "bot", kind: "activity",
|
|
7433
|
+
from: { botId: asker.id, name: asker.name, color: asker.color },
|
|
7434
|
+
tool: { name: `Waiting on your ${kind} in @${asker.name}` },
|
|
7435
|
+
threadRef: { botId: asker.id, threadId, title: store.taskByThread(asker.id, threadId)?.title ?? asker.name },
|
|
7436
|
+
});
|
|
7437
|
+
waitingOnPersonChips.set(`${threadId}:${requestId}`, { threadId: delegator.threadId, messageId: chip.id, name: asker.name, kind });
|
|
7438
|
+
}
|
|
7439
|
+
/** Settle the chip for one answered card, or every chip for this thread once
|
|
7440
|
+
* its turn ends (a card nobody answered can no longer be answered). */
|
|
7441
|
+
function settleWaitingOnPersonChips(threadId, requestId) {
|
|
7442
|
+
for (const [key, chip] of waitingOnPersonChips) {
|
|
7443
|
+
if (requestId ? key !== `${threadId}:${requestId}` : !key.startsWith(`${threadId}:`))
|
|
7444
|
+
continue;
|
|
7445
|
+
waitingOnPersonChips.delete(key);
|
|
7446
|
+
const existing = store.messagesFor(chip.threadId).find((message) => message.id === chip.messageId);
|
|
7447
|
+
if (!existing?.tool)
|
|
7448
|
+
continue;
|
|
7449
|
+
store.patchMessage(chip.threadId, chip.messageId, {
|
|
7450
|
+
tool: { ...existing.tool, name: requestId ? `@${chip.name} got your ${chip.kind}` : `@${chip.name} is no longer waiting on your ${chip.kind}`, ok: true },
|
|
7451
|
+
});
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7568
7454
|
// Peer wake: when a delegated reply lands, resume the source bot so it can
|
|
7569
7455
|
// fold the result in and answer the user instead of sitting idle. Mirrors
|
|
7570
7456
|
// the cardContinuation resume pattern used for connector/credential cards.
|
|
@@ -7608,10 +7494,7 @@ function dispatchDelegationWake(botId, threadId, targetName, failureReason, rout
|
|
|
7608
7494
|
store.appendMessage(threadId, {
|
|
7609
7495
|
role: "bot",
|
|
7610
7496
|
kind: "activity",
|
|
7611
|
-
tool: {
|
|
7612
|
-
name: `error: could not resume after delegation — ${message.slice(0, 120)}`,
|
|
7613
|
-
ok: false,
|
|
7614
|
-
},
|
|
7497
|
+
tool: failedTurnTool(`could not resume after delegation — ${message}`),
|
|
7615
7498
|
});
|
|
7616
7499
|
routines?.failThread(threadId, `Could not resume after delegation: ${message}`);
|
|
7617
7500
|
});
|
|
@@ -7713,7 +7596,7 @@ function reportIncident(input) {
|
|
|
7713
7596
|
store.appendMessage(incidents.threadId, {
|
|
7714
7597
|
role: "bot",
|
|
7715
7598
|
kind: "activity",
|
|
7716
|
-
tool:
|
|
7599
|
+
tool: failedTurnTool(`the incident could not reach ${chief.name} — ${why}`),
|
|
7717
7600
|
});
|
|
7718
7601
|
tellThePerson();
|
|
7719
7602
|
});
|
|
@@ -8112,7 +7995,7 @@ const runDelegatedTurn = (toBotId, rawText, commsDepth, sourceThreadId, channel,
|
|
|
8112
7995
|
store.appendMessage(sourceThreadId, {
|
|
8113
7996
|
role: "bot",
|
|
8114
7997
|
kind: "activity",
|
|
8115
|
-
tool:
|
|
7998
|
+
tool: failedTurnTool(`delegation to @${bot?.name ?? toBotId} could not start — ${why}`),
|
|
8116
7999
|
});
|
|
8117
8000
|
};
|
|
8118
8001
|
return startTurn(toBotId, text, {
|
|
@@ -8224,10 +8107,7 @@ function drainQueuedSends() {
|
|
|
8224
8107
|
}).catch((err) => {
|
|
8225
8108
|
store.appendMessage(threadId, {
|
|
8226
8109
|
role: "bot", kind: "activity",
|
|
8227
|
-
tool: {
|
|
8228
|
-
name: `error: queued message could not start — ${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`,
|
|
8229
|
-
ok: false,
|
|
8230
|
-
},
|
|
8110
|
+
tool: failedTurnTool(`queued message could not start — ${err instanceof Error ? err.message : String(err)}`),
|
|
8231
8111
|
});
|
|
8232
8112
|
resolve();
|
|
8233
8113
|
// M2: only this group left the queue, and a turn that never
|
|
@@ -8284,10 +8164,7 @@ function drainAsideLane() {
|
|
|
8284
8164
|
}).catch((err) => {
|
|
8285
8165
|
store.appendMessage(threadId, {
|
|
8286
8166
|
role: "bot", kind: "activity",
|
|
8287
|
-
tool: {
|
|
8288
|
-
name: `error: queued aside could not start — ${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`,
|
|
8289
|
-
ok: false,
|
|
8290
|
-
},
|
|
8167
|
+
tool: failedTurnTool(`queued aside could not start — ${err instanceof Error ? err.message : String(err)}`),
|
|
8291
8168
|
});
|
|
8292
8169
|
resolve();
|
|
8293
8170
|
}).catch(reject);
|
|
@@ -8579,7 +8456,7 @@ async function startOrQueueOpenedThread(botId, threadId, text, unattended) {
|
|
|
8579
8456
|
store.appendMessage(threadId, {
|
|
8580
8457
|
role: "bot",
|
|
8581
8458
|
kind: "activity",
|
|
8582
|
-
tool:
|
|
8459
|
+
tool: failedTurnTool(`this thread could not start — ${why}`),
|
|
8583
8460
|
});
|
|
8584
8461
|
return { state: "failed", error: why };
|
|
8585
8462
|
}
|
|
@@ -8782,11 +8659,6 @@ async function startTurn(botId, text, opts) {
|
|
|
8782
8659
|
// A workspace at its monthly spend limit starts no turn of any kind: a
|
|
8783
8660
|
// person's message, a routine, a peer hop or a webhook all stop here.
|
|
8784
8661
|
assertWithinBudget(cfg, DATA_DIR);
|
|
8785
|
-
if (checkpointRestoreLeases.has(botId)) {
|
|
8786
|
-
throw Object.assign(new Error("this bot's project files are being restored — wait for the restore to finish"), {
|
|
8787
|
-
status: 409,
|
|
8788
|
-
});
|
|
8789
|
-
}
|
|
8790
8662
|
if (boatLifecycleBusyBots.has(botId)) {
|
|
8791
8663
|
throw Object.assign(new Error("this bot's cloud computer is being changed — wait for it to finish"), { status: 409 });
|
|
8792
8664
|
}
|
|
@@ -8803,10 +8675,10 @@ async function startTurn(botId, text, opts) {
|
|
|
8803
8675
|
// their results still return here (outstandingAssignmentsPrompt tells this
|
|
8804
8676
|
// turn which are still out). Stop, in this conversation, is the gesture
|
|
8805
8677
|
// that ends coordination — see interruptDirectThread.
|
|
8806
|
-
//
|
|
8807
|
-
// integrations. Completion and interrupt paths do the same;
|
|
8808
|
-
//
|
|
8809
|
-
|
|
8678
|
+
// Revoke anything a previous turn left behind before minting this turn's
|
|
8679
|
+
// integrations. Completion and interrupt paths do the same; a proxy the
|
|
8680
|
+
// warm engine kept is honoured again only through this turn's records.
|
|
8681
|
+
revokeEarlierTurnCapabilities(threadId);
|
|
8810
8682
|
// a webhook turn, or one inherited from a bot already running unattended
|
|
8811
8683
|
if (opts?.automationSource === "webhook" || opts?.unattended)
|
|
8812
8684
|
markUnattended(bot.id, threadId);
|
|
@@ -9231,14 +9103,16 @@ async function startTurn(botId, text, opts) {
|
|
|
9231
9103
|
const pinnedCwd = privateWorkspace
|
|
9232
9104
|
? store.pinTaskCwd(bot.id, threadId, privateWorkspace, { privateOnly: cloudGuestOpened(threadId) })
|
|
9233
9105
|
: null;
|
|
9106
|
+
// A bot's threads run side by side in one project folder, like several
|
|
9107
|
+
// Claude Code or Codex sessions open in one repo: the folder is never a
|
|
9108
|
+
// turn-long claim. What truly needs one writer queues on its own — the
|
|
9109
|
+
// per-turn snapshot per folder (server/checkpoints.ts), memory through
|
|
9110
|
+
// memory_update — and the engines key their sessions by id, not folder.
|
|
9234
9111
|
const cwd = pinnedCwd ?? undefined;
|
|
9235
|
-
if (cwd && !claimTurnResource(resourceOwner, workspaceResource(cwd))) {
|
|
9236
|
-
throw Object.assign(new Error("another thread is working in this project folder — wait for it to finish or choose a separate folder"), { status: 409, code: "workspace_busy" });
|
|
9237
|
-
}
|
|
9238
9112
|
// Checkpoint explicit project folders, where a bot can overwrite the
|
|
9239
9113
|
// user's work. Its private OpenMaus workspace is app-owned and changes
|
|
9240
9114
|
// on nearly every ordinary chat; snapshotting it would add hidden disk
|
|
9241
|
-
// and process overhead without a user project to
|
|
9115
|
+
// and process overhead without a user project to list changes for.
|
|
9242
9116
|
const checkpointCwd = cwd && cwd !== privateWorkspace ? cwd : undefined;
|
|
9243
9117
|
// dweb is opt-in: without an explicit daemon URL, do not advertise
|
|
9244
9118
|
// tools that would fail on every call or spawn an unnecessary proxy.
|
|
@@ -9261,8 +9135,6 @@ async function startTurn(botId, text, opts) {
|
|
|
9261
9135
|
// landed. A team computer or a cloud routine is not this conversation's
|
|
9262
9136
|
// choice, so those ignore the pin.
|
|
9263
9137
|
const dispatchTask = store.taskByThread(bot.id, threadId);
|
|
9264
|
-
if (plan.clearPin && dispatchTask)
|
|
9265
|
-
store.patchTask(bot.id, threadId, { surface: undefined });
|
|
9266
9138
|
if (plan.computer !== undefined && plan.computer !== "cloud" && instance.adapter.capabilities.remoteAgent === true) {
|
|
9267
9139
|
throw new Error("the Computer engine works on the cloud computer — set Works on to Cloud, or choose another engine");
|
|
9268
9140
|
}
|
|
@@ -9277,16 +9149,17 @@ async function startTurn(botId, text, opts) {
|
|
|
9277
9149
|
// Where an explicit place came from, so its failure names the one
|
|
9278
9150
|
// control that changes it (and a failed Auto-recorded pin is cleared).
|
|
9279
9151
|
// Auto and a team's shared computer are nobody's per-turn choice.
|
|
9280
|
-
const
|
|
9152
|
+
const namedPlace = teamComputer || wants === undefined || wants === "off" ? undefined : wants;
|
|
9153
|
+
const placeSource = !namedPlace ? undefined
|
|
9281
9154
|
: opts?.runOn === "cloud" ? "routine"
|
|
9282
9155
|
: plan.pinned ? (dispatchTask?.surfaceSource === "auto" ? "auto-pin" : "pin")
|
|
9283
9156
|
: "works-on";
|
|
9284
9157
|
// An engine that cannot use the cloud computer is refused here, before
|
|
9285
9158
|
// anything is created or woken — never handed to another engine.
|
|
9286
9159
|
if (wants === "cloud" && placeSource) {
|
|
9287
|
-
const unsupported = cloudPlaceDriverError(instance
|
|
9160
|
+
const unsupported = cloudPlaceDriverError(cloudEngine(instance), cloudBackend, placeSource);
|
|
9288
9161
|
if (unsupported)
|
|
9289
|
-
throw new PlaceUnavailableError("cloud",
|
|
9162
|
+
throw new PlaceUnavailableError("cloud", unsupported);
|
|
9290
9163
|
}
|
|
9291
9164
|
let previewCapture = null;
|
|
9292
9165
|
let browserCapture = null;
|
|
@@ -9435,7 +9308,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9435
9308
|
store.appendMessage(threadId, {
|
|
9436
9309
|
role: "bot",
|
|
9437
9310
|
kind: "activity",
|
|
9438
|
-
tool:
|
|
9311
|
+
tool: failedTurnTool(message),
|
|
9439
9312
|
});
|
|
9440
9313
|
if (opts?.automationSource === undefined && !opts?.commsDepth && !opts?.cardContinuation) {
|
|
9441
9314
|
notify(buildNotification("turn-failed", bot, threadId, redactSecretsInText(message), { avatarUrl: bot.avatarUrl }));
|
|
@@ -9557,7 +9430,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9557
9430
|
// the person explicitly opted this bot into remote lifecycle actions.
|
|
9558
9431
|
// On Auto an engine that cannot use a computer simply skips it.
|
|
9559
9432
|
if ((wants === "cloud" || (wants === undefined && managedPolicy.computerAllowed("vps"))) && cloudBackend === "vps" &&
|
|
9560
|
-
|
|
9433
|
+
canWorkOnCloud(cloudEngine(instance), "vps")) {
|
|
9561
9434
|
// The VPS "computer" is the desktop inside this bot's managed
|
|
9562
9435
|
// container, and only screen work needs that desktop to itself.
|
|
9563
9436
|
// So the lease is claimed on the first computer call, through the
|
|
@@ -9595,26 +9468,27 @@ async function startTurn(botId, text, opts) {
|
|
|
9595
9468
|
autoVpsProblem = mounted.problem ?? "the VPS computer could not be reached";
|
|
9596
9469
|
}
|
|
9597
9470
|
}
|
|
9598
|
-
// Cloud is strict when selected. Every engine that can use it may also
|
|
9599
|
-
// reuse an already-ready Boat on Auto.
|
|
9600
9471
|
if (teamComputer) {
|
|
9601
|
-
const attached = await attachTeamBoat(teamComputer, bot.id, resourceOwner,
|
|
9472
|
+
const attached = await attachTeamBoat(teamComputer, bot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance), "box"), instance.adapter.capabilities.remoteAgent === true);
|
|
9602
9473
|
Object.assign(integrations, attached.mount);
|
|
9603
9474
|
previewCapture = attached.capture;
|
|
9604
9475
|
computerKind = "box";
|
|
9605
9476
|
}
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9477
|
+
// Cloud is strict when selected. On Auto only the Computer engine,
|
|
9478
|
+
// whose whole turn runs on its Boat, attaches one, and only one that
|
|
9479
|
+
// is already running. Every other engine reaches the Boat only when
|
|
9480
|
+
// this conversation is on it (Works on, a pin, a cloud routine, or
|
|
9481
|
+
// select_computer): an Auto turn never reads the Boat account, so it
|
|
9482
|
+
// adds no Boat call, cannot be stopped by a Boat being deleted, and
|
|
9483
|
+
// never records a place nobody chose.
|
|
9484
|
+
const remoteAgent = instance.adapter.capabilities.remoteAgent === true;
|
|
9485
|
+
if (!teamComputer && cloudBackend === "box" && boat.boatConfigured(cfg) &&
|
|
9486
|
+
(wants === "cloud" || (wants === undefined && remoteAgent && !computerPlaceRefusal("box")))) {
|
|
9487
|
+
const attached = await attachBotBoat(bot, resourceOwner, { explicitCloud: wants === "cloud", remoteAgent });
|
|
9612
9488
|
if (attached) {
|
|
9613
9489
|
previewCapture = attached.capture;
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
computerKind = "box";
|
|
9617
|
-
}
|
|
9490
|
+
Object.assign(integrations, attached.mount);
|
|
9491
|
+
computerKind = "box";
|
|
9618
9492
|
}
|
|
9619
9493
|
}
|
|
9620
9494
|
if (wants === "cloud" && cloudBackend === "box" && !boat.boatConfigured(cfg)) {
|
|
@@ -9625,10 +9499,11 @@ async function startTurn(botId, text, opts) {
|
|
|
9625
9499
|
}
|
|
9626
9500
|
}
|
|
9627
9501
|
catch (error) {
|
|
9628
|
-
|
|
9629
|
-
|
|
9502
|
+
// Auto, Off and a team's shared computer name no place; they fail as
|
|
9503
|
+
// they are.
|
|
9504
|
+
if (!namedPlace || !placeSource)
|
|
9630
9505
|
throw error;
|
|
9631
|
-
throw
|
|
9506
|
+
throw asPlaceFailure(error, namedPlace, placeSource);
|
|
9632
9507
|
}
|
|
9633
9508
|
// Auto-only host fallback. Electron owns cua-driver/TCC attribution;
|
|
9634
9509
|
// the harness only reads its already-running connection descriptor.
|
|
@@ -9671,7 +9546,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9671
9546
|
pinAutoSurface("local");
|
|
9672
9547
|
},
|
|
9673
9548
|
});
|
|
9674
|
-
integrations.localComputer = gatedLocalComputer(cua, controlIntegration(bot.id, threadId, dispatchClaimId));
|
|
9549
|
+
integrations.localComputer = gatedLocalComputer(cua, controlIntegration(bot.id, threadId, dispatchClaimId, "this computer"));
|
|
9675
9550
|
computerKind = "local";
|
|
9676
9551
|
}
|
|
9677
9552
|
}
|
|
@@ -9739,14 +9614,17 @@ async function startTurn(botId, text, opts) {
|
|
|
9739
9614
|
// snapshot() absorbs failures, so checkpointing may delay but never fail
|
|
9740
9615
|
// a turn.
|
|
9741
9616
|
if (checkpointCwd) {
|
|
9742
|
-
|
|
9617
|
+
// Pinned by this dispatch: a sibling thread's snapshot in the same
|
|
9618
|
+
// folder must not reclaim this turn's "before" state while it runs.
|
|
9619
|
+
const before = await checkpoints.snapshot(bot.id, checkpointCwd, `turn ${threadId.slice(0, 8)}`, undefined, { pin: dispatchClaimId });
|
|
9743
9620
|
if (!directTurnClaimIsCurrent(bot.id, dispatchClaimId, threadId)) {
|
|
9621
|
+
if (before)
|
|
9622
|
+
void checkpoints.release(bot.id, checkpointCwd, dispatchClaimId);
|
|
9744
9623
|
throw new DirectTurnSetupCancelled("turn stopped during checkpoint");
|
|
9745
9624
|
}
|
|
9625
|
+
forgetTurnCheckpoint(threadId);
|
|
9746
9626
|
if (before)
|
|
9747
|
-
turnCheckpoints.set(threadId, { cwd: checkpointCwd, hash: before });
|
|
9748
|
-
else
|
|
9749
|
-
turnCheckpoints.delete(threadId);
|
|
9627
|
+
turnCheckpoints.set(threadId, { botId: bot.id, cwd: checkpointCwd, hash: before, pin: dispatchClaimId });
|
|
9750
9628
|
}
|
|
9751
9629
|
if (!directTurnClaimIsCurrent(bot.id, dispatchClaimId, threadId)) {
|
|
9752
9630
|
throw new DirectTurnSetupCancelled("turn stopped before dispatch");
|
|
@@ -9805,8 +9683,9 @@ async function startTurn(botId, text, opts) {
|
|
|
9805
9683
|
// Pin on use (issue #1650): every computer that registers a claim
|
|
9806
9684
|
// slot — a ready Local VM, the VPS, the host fallback, a VM created
|
|
9807
9685
|
// for this turn — records its pin inside the claim itself, so a turn
|
|
9808
|
-
// that only mounted tools records nothing.
|
|
9809
|
-
//
|
|
9686
|
+
// that only mounted tools records nothing. The Computer engine's Boat
|
|
9687
|
+
// (its whole turn runs there) and the built-in browser (no seat to
|
|
9688
|
+
// claim) pin here.
|
|
9810
9689
|
if (autoPinAllowed()) {
|
|
9811
9690
|
const claimSlot = autoVmClaims.get(threadId);
|
|
9812
9691
|
const pinsAtClaim = claimSlot?.owner.generation === resourceOwner.generation;
|
|
@@ -9861,6 +9740,9 @@ async function startTurn(botId, text, opts) {
|
|
|
9861
9740
|
{ id: "learn", label: "Skill authoring", text: learnPrompt },
|
|
9862
9741
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
9863
9742
|
{ id: "team-memory", label: "Team memory", text: teamMemory.systemPrompt(bot.section) + (integrations.agents ? TEAM_MEMORY_PROMPT : "") },
|
|
9743
|
+
// who on the team roster is busy right now; volatile, so a teammate
|
|
9744
|
+
// starting or finishing work never changes the stable half
|
|
9745
|
+
teamAvailabilityPart(coordinationPrompt ? sectionPeers : []),
|
|
9864
9746
|
// what the bot said lately in its other conversations, so a task
|
|
9865
9747
|
// never redoes — or forgets — what another one already did
|
|
9866
9748
|
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentWork(recentWorkSources(bot), bot, { userName: cfg.profile?.name?.trim() || "User", currentThreadId: threadId, ...recentWorkFilter() })) },
|
|
@@ -10132,7 +10014,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10132
10014
|
role: "bot",
|
|
10133
10015
|
kind: "activity",
|
|
10134
10016
|
turnSucceeded: false,
|
|
10135
|
-
tool:
|
|
10017
|
+
tool: failedTurnTool(message),
|
|
10136
10018
|
});
|
|
10137
10019
|
// Worth a buzz for the same reason a routine failure is, and the rule
|
|
10138
10020
|
// notify.ts encodes: the bot is not working, and the cause is usually
|
|
@@ -10400,9 +10282,10 @@ routines = new RoutineManager({
|
|
|
10400
10282
|
// nobody's is confined to a folder of its own, never the bot's project.
|
|
10401
10283
|
if (task && CLOUD_HOME && routineId)
|
|
10402
10284
|
threadStarters.set(task.threadId, routineOpener(routineId));
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10285
|
+
// The store's frame announces it. A run's task that stays in the
|
|
10286
|
+
// background leaves the open thread alone, so that frame carries no
|
|
10287
|
+
// transcript: a whole thread on every scheduled run once passed the phone
|
|
10288
|
+
// sidecar's 4 MiB event ceiling and ended the stream (MOCA-179).
|
|
10406
10289
|
return task;
|
|
10407
10290
|
},
|
|
10408
10291
|
joinConversation: (run) => {
|
|
@@ -10457,11 +10340,8 @@ routines = new RoutineManager({
|
|
|
10457
10340
|
discardDelegations(commsBus, threadId);
|
|
10458
10341
|
cancelDirectTurnDispatch(botId, threadId);
|
|
10459
10342
|
revokeInternalCapabilitiesForThread(threadId);
|
|
10460
|
-
const instance = bot
|
|
10461
|
-
? runningTurnInstance(bot, threadId)
|
|
10462
|
-
: runningTurnEngines.get(threadId) ?? null;
|
|
10463
10343
|
try {
|
|
10464
|
-
await
|
|
10344
|
+
await runningTurnInstance(bot, threadId)?.adapter.interruptTurn(threadId);
|
|
10465
10345
|
}
|
|
10466
10346
|
finally {
|
|
10467
10347
|
closeOpenApprovals(threadId);
|
|
@@ -10624,9 +10504,9 @@ async function cloudRoutineReadiness(botId, threadId) {
|
|
|
10624
10504
|
if (!instance)
|
|
10625
10505
|
return { ready: false, reason: "The target bot's model is unavailable. Choose another model in the bot's settings." };
|
|
10626
10506
|
// The same rule a cloud turn applies, before anything is provisioned.
|
|
10627
|
-
const unsupported = cloudPlaceDriverError(instance
|
|
10507
|
+
const unsupported = cloudPlaceDriverError(cloudEngine(instance), "box", "routine");
|
|
10628
10508
|
if (unsupported)
|
|
10629
|
-
return { ready: false, reason:
|
|
10509
|
+
return { ready: false, reason: unsupported };
|
|
10630
10510
|
try {
|
|
10631
10511
|
if ((await instance.snapshot()).state !== "available") {
|
|
10632
10512
|
return { ready: false, reason: "The target bot's model is not ready to use the Boat cloud computer." };
|
|
@@ -10998,52 +10878,6 @@ const modelRequests = new ModelRequestService({
|
|
|
10998
10878
|
validateModel: validateModelProposal,
|
|
10999
10879
|
driverCapabilities: driverCapabilitiesFor,
|
|
11000
10880
|
});
|
|
11001
|
-
const tighteningRequests = new TighteningRequestService({
|
|
11002
|
-
store,
|
|
11003
|
-
autoApply: fullAccessForSource,
|
|
11004
|
-
canPersist: proposalPersistence,
|
|
11005
|
-
// Same reach as a profile change: a Chief may tighten a section peer;
|
|
11006
|
-
// anyone else only itself. Re-checked at confirm.
|
|
11007
|
-
validateTarget: (proposerBotId, targetBotId) => {
|
|
11008
|
-
const proposer = store.bot(proposerBotId);
|
|
11009
|
-
const target = store.bot(targetBotId);
|
|
11010
|
-
if (!target)
|
|
11011
|
-
return "that bot no longer exists";
|
|
11012
|
-
if (!proposer?.chiefOfStaff)
|
|
11013
|
-
return "only a section's Chief of Staff can change another bot's permissions";
|
|
11014
|
-
if (!canReachPeer(proposer, target))
|
|
11015
|
-
return "that bot is not in a team this Chief is allowed to manage";
|
|
11016
|
-
return null;
|
|
11017
|
-
},
|
|
11018
|
-
// The card judges the effective mounts, so the snapshot resolves the same
|
|
11019
|
-
// way the engine does: through the config-aware custom-server filter.
|
|
11020
|
-
mountedMcpServers: (bot) => Object.keys(engineMcpServers(bot)),
|
|
11021
|
-
enabledSkills: (botId) => {
|
|
11022
|
-
const record = store.bot(botId);
|
|
11023
|
-
return (record ? listBotSkills(record) : listSkills(botId)).filter((skill) => skill.enabled).map((skill) => skill.name);
|
|
11024
|
-
},
|
|
11025
|
-
disableSkill: (botId, name) => {
|
|
11026
|
-
const record = store.bot(botId);
|
|
11027
|
-
// Private skills win collisions, including disabled/private copies.
|
|
11028
|
-
// Disabling one must not reveal a same-named shared skill instead.
|
|
11029
|
-
if (skillsLibraryEnabled(cfg) && !listSkills(botId).some((skill) => skill.name === name)
|
|
11030
|
-
&& record?.assignedSkills?.includes(name)) {
|
|
11031
|
-
try {
|
|
11032
|
-
const saved = store.patchBot(botId, { assignedSkills: record.assignedSkills.filter((assigned) => assigned !== name) });
|
|
11033
|
-
return saved ? { ok: true } : { ok: false, error: "that bot no longer exists" };
|
|
11034
|
-
}
|
|
11035
|
-
catch (error) {
|
|
11036
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
11037
|
-
}
|
|
11038
|
-
}
|
|
11039
|
-
const disabled = setSkillEnabled(botId, name, false);
|
|
11040
|
-
return typeof disabled === "object" && "error" in disabled ? { ok: false, error: disabled.error } : { ok: true };
|
|
11041
|
-
},
|
|
11042
|
-
// The owner's own routes stop a busy bot before touching its approval
|
|
11043
|
-
// level or MCP mounts; the card asks for the same, except an emergency
|
|
11044
|
-
// full/custom → ask downgrade, which applies first and stops after.
|
|
11045
|
-
targetBusy: (botId) => Boolean(store.bot(botId)?.busy || activeGroupTurnForBot(botId)),
|
|
11046
|
-
});
|
|
11047
10881
|
const teamSetupTeams = () => [...new Set(["", ...readSections(), ...store.bots.map((bot) => sectionKey(bot.section)), ...store.groups.map((group) => sectionKey(group.section))])];
|
|
11048
10882
|
const teamSetupRequests = new TeamSetupRequestService({
|
|
11049
10883
|
store, teams: teamSetupTeams, canAccessTeam, canPersist: proposalPersistence, maxBots: MAX_WORKSPACE_BOTS,
|
|
@@ -11355,46 +11189,6 @@ function resolveAndSendModel(res, args) {
|
|
|
11355
11189
|
json(res, 200, { ok: true, outcome: "rejected" });
|
|
11356
11190
|
return true;
|
|
11357
11191
|
}
|
|
11358
|
-
function resolveAndSendTightening(res, args) {
|
|
11359
|
-
const card = store.messagesFor(args.threadId).find((message) => message.card?.requestId === args.requestId && message.card.tighteningRequest)?.card;
|
|
11360
|
-
if (!card)
|
|
11361
|
-
return false;
|
|
11362
|
-
const result = tighteningRequests.resolve(args);
|
|
11363
|
-
if (!result.claimed)
|
|
11364
|
-
return false;
|
|
11365
|
-
if (result.state === "applied" || result.state === "denied") {
|
|
11366
|
-
appendDecision(DATA_DIR, {
|
|
11367
|
-
threadId: args.threadId, requestId: args.requestId, botId: args.botId, botName: args.botName,
|
|
11368
|
-
tool: "tighten_permissions", summary: card.subtitle,
|
|
11369
|
-
decision: result.state === "applied" ? "user-approved" : "user-denied", source: "user",
|
|
11370
|
-
});
|
|
11371
|
-
}
|
|
11372
|
-
if (result.state === "applied") {
|
|
11373
|
-
const target = store.bot(result.targetBotId);
|
|
11374
|
-
if (target)
|
|
11375
|
-
broadcast({ kind: "bot", bot: wireBot(target) });
|
|
11376
|
-
if (result.emergencyStop) {
|
|
11377
|
-
// Mirror the owner's own route: the fail-closed Ask is already
|
|
11378
|
-
// persisted, then the exact turn still holding the elevated
|
|
11379
|
-
// per-turn snapshot is stopped. The response never waits on it.
|
|
11380
|
-
void stopBotForEmergencyApprovalDowngrade(result.targetBotId).catch((error) => {
|
|
11381
|
-
console.warn(`[tightening] emergency stop failed for bot ${result.targetBotId}: ${error instanceof Error ? error.message : String(error)}. The fail-closed Ask is persisted, but the in-flight turn keeps its elevated access until it ends.`);
|
|
11382
|
-
});
|
|
11383
|
-
}
|
|
11384
|
-
json(res, 200, { ok: true, outcome: "allowed-once", tighteningFields: result.fields });
|
|
11385
|
-
return true;
|
|
11386
|
-
}
|
|
11387
|
-
if (result.state === "invalid") {
|
|
11388
|
-
json(res, result.status, { error: result.error });
|
|
11389
|
-
return true;
|
|
11390
|
-
}
|
|
11391
|
-
if (result.state === "already_settled") {
|
|
11392
|
-
json(res, 200, { ok: true, outcome: result.behavior === "allow" ? "allowed-once" : "rejected", alreadySettled: true });
|
|
11393
|
-
return true;
|
|
11394
|
-
}
|
|
11395
|
-
json(res, 200, { ok: true, outcome: "rejected" });
|
|
11396
|
-
return true;
|
|
11397
|
-
}
|
|
11398
11192
|
// Webhook definitions are independent from calendar schedules, but every
|
|
11399
11193
|
// delivery joins the same RoutineManager queue. That keeps unattended work
|
|
11400
11194
|
// ordered behind a busy MAUS and gives webhook runs the same durable receipts.
|
|
@@ -11589,7 +11383,7 @@ setupRetry = 0) {
|
|
|
11589
11383
|
onDispatchError?.(`${bot.name}'s approval level is still being confirmed — skipped this round`);
|
|
11590
11384
|
return true;
|
|
11591
11385
|
}
|
|
11592
|
-
|
|
11386
|
+
revokeEarlierTurnCapabilities(threadId);
|
|
11593
11387
|
spoken.add(botId);
|
|
11594
11388
|
// Must be the SAME resolver the readiness re-check uses below, or a Chief's
|
|
11595
11389
|
// delegated Full elevation makes the two disagree by construction: every
|
|
@@ -11617,7 +11411,7 @@ setupRetry = 0) {
|
|
|
11617
11411
|
role: "bot",
|
|
11618
11412
|
kind: "activity",
|
|
11619
11413
|
from: { botId: bot.id, name: bot.name, color: bot.color },
|
|
11620
|
-
tool:
|
|
11414
|
+
tool: failedTurnTool(message),
|
|
11621
11415
|
});
|
|
11622
11416
|
onDispatchError?.(message);
|
|
11623
11417
|
return true;
|
|
@@ -11749,7 +11543,7 @@ setupRetry = 0) {
|
|
|
11749
11543
|
role: "bot",
|
|
11750
11544
|
kind: "activity",
|
|
11751
11545
|
from: { botId: bot.id, name: bot.name, color: bot.color },
|
|
11752
|
-
tool:
|
|
11546
|
+
tool: failedTurnTool(message),
|
|
11753
11547
|
});
|
|
11754
11548
|
onDispatchError?.(message);
|
|
11755
11549
|
return true;
|
|
@@ -11906,9 +11700,9 @@ setupRetry = 0) {
|
|
|
11906
11700
|
// The same one rule as a bot thread, before anything is provisioned: an
|
|
11907
11701
|
// engine that cannot use the cloud computer is refused, never swapped.
|
|
11908
11702
|
if (!roomTeamComputer && roomPlan.computer === "cloud") {
|
|
11909
|
-
const unsupported = cloudPlaceDriverError(instance
|
|
11703
|
+
const unsupported = cloudPlaceDriverError(cloudEngine(instance), turnProvider(readyBot) === "vps" ? "vps" : "box");
|
|
11910
11704
|
if (unsupported)
|
|
11911
|
-
throw new PlaceUnavailableError("cloud",
|
|
11705
|
+
throw new PlaceUnavailableError("cloud", unsupported);
|
|
11912
11706
|
}
|
|
11913
11707
|
// One place per room turn as well: a team computer reached on Auto means
|
|
11914
11708
|
// no separate built-in browser.
|
|
@@ -11928,7 +11722,7 @@ setupRetry = 0) {
|
|
|
11928
11722
|
return false;
|
|
11929
11723
|
}
|
|
11930
11724
|
if (roomTeamComputer) {
|
|
11931
|
-
const attached = await attachTeamBoat(roomTeamComputer, readyBot.id, resourceOwner,
|
|
11725
|
+
const attached = await attachTeamBoat(roomTeamComputer, readyBot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance), "box"), instance.adapter.capabilities.remoteAgent === true);
|
|
11932
11726
|
if (isCancelled?.() || groupSpeakers.get(threadId) !== roomSpeaker ||
|
|
11933
11727
|
activeInternalGenerationByThread.get(threadId) !== internalGeneration)
|
|
11934
11728
|
return false;
|
|
@@ -11974,11 +11768,10 @@ setupRetry = 0) {
|
|
|
11974
11768
|
if (!boat.boatConfigured(cfg))
|
|
11975
11769
|
throw new Error(BOAT_NOT_CONFIGURED);
|
|
11976
11770
|
const remoteAgent = instance.adapter.capabilities.remoteAgent === true;
|
|
11977
|
-
const attached = await attachBotBoat(readyBot, resourceOwner, { explicitCloud: true,
|
|
11978
|
-
canMount: usesCloudComputer(instance.adapter.capabilities), remoteAgent });
|
|
11771
|
+
const attached = await attachBotBoat(readyBot, resourceOwner, { explicitCloud: true, remoteAgent });
|
|
11979
11772
|
if (!roomSetupIsCurrent())
|
|
11980
11773
|
return false;
|
|
11981
|
-
if (!attached
|
|
11774
|
+
if (!attached)
|
|
11982
11775
|
throw new Error("the cloud computer could not be created or reached");
|
|
11983
11776
|
Object.assign(integrations, attached.mount);
|
|
11984
11777
|
roomScreenBotId = readyBot.id;
|
|
@@ -12023,17 +11816,15 @@ setupRetry = 0) {
|
|
|
12023
11816
|
if (owner?.threadId !== threadId || owner.botId !== readyBot.id) {
|
|
12024
11817
|
throw new Error("the Local VM lease expired while preparing the turn");
|
|
12025
11818
|
}
|
|
12026
|
-
integrations.localComputer = containerComputerMcp(vm.runtime, controlIntegration(readyBot.id, threadId, internalGeneration, target), target);
|
|
11819
|
+
integrations.localComputer = containerComputerMcp(vm.runtime, controlIntegration(readyBot.id, threadId, internalGeneration, { localVmTarget: target }), target);
|
|
12027
11820
|
}
|
|
12028
11821
|
}
|
|
12029
11822
|
catch (error) {
|
|
12030
11823
|
const place = roomPlan.computer;
|
|
12031
11824
|
if (place !== "cloud" && place !== "vm" && place !== "local")
|
|
12032
11825
|
throw error;
|
|
12033
|
-
if (error instanceof DirectTurnSetupCancelled || error instanceof ComputerWaitParked || error instanceof PlaceUnavailableError)
|
|
12034
|
-
throw error;
|
|
12035
11826
|
// A room row is not cut, so the cause keeps all its words.
|
|
12036
|
-
throw
|
|
11827
|
+
throw asPlaceFailure(error, place, "works-on", Infinity);
|
|
12037
11828
|
}
|
|
12038
11829
|
const roster = readyGroup.memberIds
|
|
12039
11830
|
.map((id) => store.bot(id))
|
|
@@ -12044,8 +11835,9 @@ setupRetry = 0) {
|
|
|
12044
11835
|
// are who it cannot. The 1:1 prompt has carried a peer roster since #774,
|
|
12045
11836
|
// and a room turn had nothing — the only advice it gave ("mention them
|
|
12046
11837
|
// like @Name") sends the model after a teammate who will never see it.
|
|
12047
|
-
// Same reachability rule as list_bots, minus the room's own members
|
|
12048
|
-
|
|
11838
|
+
// Same reachability rule as list_bots, minus the room's own members, and
|
|
11839
|
+
// only on a turn that is not answering a room request.
|
|
11840
|
+
const outsideRoom = integrations.agents && orchestration && !orchestration.roomHandoffId
|
|
12049
11841
|
? reachablePeers(store.bots, bot).filter((peer) => !readyGroup.memberIds.includes(peer.id))
|
|
12050
11842
|
: [];
|
|
12051
11843
|
const system = [
|
|
@@ -12055,7 +11847,7 @@ setupRetry = 0) {
|
|
|
12055
11847
|
`Room members: ${roster}, and ${userName} (the human).`,
|
|
12056
11848
|
readyGroup.bulletin.trim() && `Room bulletin (shared instructions for everyone):\n${readyGroup.bulletin.trim()}`,
|
|
12057
11849
|
`Reply as yourself, briefly and conversationally. To bring a teammate in, mention them like @Name — they'll see the conversation and respond.`,
|
|
12058
|
-
outsideRoom.length > 0 &&
|
|
11850
|
+
outsideRoom.length > 0 && roomPeerRosterSystemPrompt(outsideRoom),
|
|
12059
11851
|
integrations.agents && (CREDENTIAL_PROMPT + (orchestration && !orchestration.roomHandoffId ? THREADS_PROMPT : "")).trim(),
|
|
12060
11852
|
integrations.agents && (!orchestration || orchestration.roomHandoffId) && "For actual OpenMausBot teamwork, discover IDs with list_room_targets and use coordinate_bots for advice or work in this or another room. Do not substitute native coding helpers for these named bots. Consult only when needed to make a decision; no discussion step is mandatory. Give concrete responsibilities, exact accessible paths and acceptance checks. End your turn after assigning; busy teammates queue and results automatically resume you. When they return, finish the requested verification and give the user one final answer. Native helper names are not evidence that an OpenMausBot teammate participated. Plain @mentions are only for conversational replies in this room.",
|
|
12061
11853
|
integrations.agents && ROUTINE_PROMPT.trim(),
|
|
@@ -12143,6 +11935,7 @@ setupRetry = 0) {
|
|
|
12143
11935
|
{ id: "cloud-home", label: "OMB Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
|
|
12144
11936
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
12145
11937
|
{ id: "recall", label: "Recall", text: integrations.agents && bot.memoryEnabled !== false ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
|
|
11938
|
+
teamAvailabilityPart(outsideRoom),
|
|
12146
11939
|
{ id: "recent", label: "Recent work", text: recentWorkPrompt(recentLines) },
|
|
12147
11940
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
12148
11941
|
{ id: "team-memory", label: "Team memory", text: teamMemory.systemPrompt(bot.section) + (integrations.agents ? TEAM_MEMORY_PROMPT : "") },
|
|
@@ -12323,7 +12116,7 @@ setupRetry = 0) {
|
|
|
12323
12116
|
role: "bot",
|
|
12324
12117
|
kind: "activity",
|
|
12325
12118
|
from: { botId: bot.id, name: bot.name, color: bot.color },
|
|
12326
|
-
tool:
|
|
12119
|
+
tool: failedTurnTool(message),
|
|
12327
12120
|
});
|
|
12328
12121
|
onDispatchError?.(message);
|
|
12329
12122
|
watchdog.settle(threadId);
|
|
@@ -12516,7 +12309,7 @@ setupRetry = 0) {
|
|
|
12516
12309
|
store.appendMessage(threadId, {
|
|
12517
12310
|
role: "bot", kind: "activity",
|
|
12518
12311
|
from: { botId: bot.id, name: bot.name, color: bot.color },
|
|
12519
|
-
tool:
|
|
12312
|
+
tool: failedTurnTool(message),
|
|
12520
12313
|
});
|
|
12521
12314
|
onDispatchError?.(message);
|
|
12522
12315
|
if (orchestration) {
|
|
@@ -13125,10 +12918,7 @@ function drainQueuedChannelSends() {
|
|
|
13125
12918
|
store.appendMessage(threadId, {
|
|
13126
12919
|
role: "bot",
|
|
13127
12920
|
kind: "activity",
|
|
13128
|
-
tool: {
|
|
13129
|
-
name: `error: queued channel message could not start — ${(error instanceof Error ? error.message : String(error)).slice(0, 120)}`,
|
|
13130
|
-
ok: false,
|
|
13131
|
-
},
|
|
12921
|
+
tool: failedTurnTool(`queued channel message could not start — ${error instanceof Error ? error.message : String(error)}`),
|
|
13132
12922
|
});
|
|
13133
12923
|
}
|
|
13134
12924
|
// A message with no eligible responder creates no operation. Continue
|
|
@@ -13335,9 +13125,9 @@ function proposalPersistence(botId, threadId) {
|
|
|
13335
13125
|
return { ok: true };
|
|
13336
13126
|
// Only cards on the visible branch can be acted on from the composer.
|
|
13337
13127
|
// Abandoned branches must not permanently consume the proposal quota.
|
|
13338
|
-
// Routine, profile, default-model
|
|
13128
|
+
// Routine, profile, default-model and team-setup proposals
|
|
13339
13129
|
// share one budget per bot per thread, so one thread cannot pile up 8 of each.
|
|
13340
|
-
const openRequests = store.activePath(threadId).filter((message) => (message.card?.routineRequest?.botId === botId || message.card?.profileRequest?.botId === botId || message.card?.modelRequest?.botId === botId || message.card?.
|
|
13130
|
+
const openRequests = store.activePath(threadId).filter((message) => (message.card?.routineRequest?.botId === botId || message.card?.profileRequest?.botId === botId || message.card?.modelRequest?.botId === botId || message.card?.teamSetupRequest?.botId === botId) &&
|
|
13341
13131
|
!message.card.answered &&
|
|
13342
13132
|
!message.card.dismissed && !message.card.expired).length;
|
|
13343
13133
|
return openRequests >= 8
|
|
@@ -14199,7 +13989,8 @@ function configStatus() {
|
|
|
14199
13989
|
// not a saved key
|
|
14200
13990
|
box: boat.describeBoatAccount(cfg),
|
|
14201
13991
|
vps: { configured: Boolean(vpsSshAlias(cfg)), sshAlias: vpsSshAlias(cfg) ?? "" },
|
|
14202
|
-
|
|
13992
|
+
// names only: every provider key stays write-only
|
|
13993
|
+
opencodeGo: { configured: Boolean(cfg.opencodeGo?.apiKey), providerKeys: Object.keys(openCodeProviderKeys(cfg)) },
|
|
14203
13994
|
// the chosen voice is a setting, not a secret; the key is reported the
|
|
14204
13995
|
// same configured-or-not way as every other credential
|
|
14205
13996
|
tts: tts.describeVoice(cfg),
|
|
@@ -14281,6 +14072,7 @@ function configForAccess(status, admin) {
|
|
|
14281
14072
|
edition: { edition: status.edition.edition, features: status.edition.features },
|
|
14282
14073
|
signIn: { admins: [], members: [] },
|
|
14283
14074
|
vps: { configured: status.vps.configured, sshAlias: "" },
|
|
14075
|
+
opencodeGo: { configured: status.opencodeGo.configured, providerKeys: [] },
|
|
14284
14076
|
profile: { name: status.profile.name, email: "" },
|
|
14285
14077
|
browserProfiles: status.browserProfiles.map((profile) => Object.fromEntries(Object.entries(profile).filter(([key]) => key !== "partitionId"))),
|
|
14286
14078
|
};
|
|
@@ -14508,7 +14300,7 @@ async function reloadProviders() {
|
|
|
14508
14300
|
if (store.taskByThread(botId, threadId)) {
|
|
14509
14301
|
store.appendMessage(threadId, {
|
|
14510
14302
|
role: "bot", kind: "activity",
|
|
14511
|
-
tool:
|
|
14303
|
+
tool: failedTurnTool("turn interrupted — provider settings changed"),
|
|
14512
14304
|
});
|
|
14513
14305
|
store.setTaskActivity(botId, threadId, "idle");
|
|
14514
14306
|
}
|
|
@@ -14611,7 +14403,7 @@ const workspaceBackupAccess = {
|
|
|
14611
14403
|
!routines?.isTicking && !calendarCalls?.isTicking &&
|
|
14612
14404
|
!localVmImageBusy && !localVmProvisionBusy && !localVmModeChangeBusy &&
|
|
14613
14405
|
!localVmLifecycleBusy.size && !boatLifecycleBusyBots.size && !vpsPreviewRequests.size && !orphanBoatLifecycleBusyIds.size &&
|
|
14614
|
-
!computerProviderConfigTransitions.size &&
|
|
14406
|
+
!computerProviderConfigTransitions.size &&
|
|
14615
14407
|
teamComputers.list().every(computer => !teamComputerInUse(computer)) &&
|
|
14616
14408
|
store.bots.every((bot) => !botHasActiveTurn(bot.id) && !routines?.activeRunForBot(bot.id) && !botComputerControlSnapshot(bot.id).held) &&
|
|
14617
14409
|
store.groups.every((group) => !groupIsWorking(group)),
|
|
@@ -14634,18 +14426,24 @@ const workspaceBackupRoutes = createWorkspaceBackupRoutes({
|
|
|
14634
14426
|
restored: workspaceRestore,
|
|
14635
14427
|
...workspaceBackupAccess,
|
|
14636
14428
|
});
|
|
14637
|
-
//
|
|
14638
|
-
//
|
|
14639
|
-
//
|
|
14640
|
-
|
|
14429
|
+
// Copy this computer here (server/cloud-move-http.ts, docs/copy-workspace.md):
|
|
14430
|
+
// every server sizes its own workspace for its desktop, and every server the
|
|
14431
|
+
// person owns receives one, a Cloud home included. A workspace shared with
|
|
14432
|
+
// other people never does (workspaceShared): a hosted organisation
|
|
14433
|
+
// workspace, or a server whose email sign-in lets someone besides its owner
|
|
14434
|
+
// in. After the restore commits, the server
|
|
14435
|
+
// exits with RESTART_EXIT_CODE and its launcher starts it again, so startup
|
|
14436
|
+
// installs it (server/restart.ts).
|
|
14437
|
+
let restartRequested = false;
|
|
14641
14438
|
const cloudMoveRoutes = createCloudMoveRoutes({
|
|
14642
14439
|
dataDir: DATA_DIR,
|
|
14643
14440
|
appVersion: serverVersion(),
|
|
14644
|
-
|
|
14441
|
+
environmentId: ENVIRONMENT_ID,
|
|
14442
|
+
sharedWorkspace: () => workspaceShared({ hosted: HOSTED_WORKSPACE, cloudHome: Boolean(CLOUD_HOME), signIn: signInAllowList() }),
|
|
14645
14443
|
readBody,
|
|
14646
14444
|
restored: workspaceRestore,
|
|
14647
14445
|
...workspaceBackupAccess,
|
|
14648
|
-
restart: () => {
|
|
14446
|
+
restart: () => { restartRequested = true; gracefulShutdown(); },
|
|
14649
14447
|
});
|
|
14650
14448
|
// Route modules (server/routes/README.md). `workspaceAccess` is assigned at
|
|
14651
14449
|
// boot, after this line, so the dependency reads it per request.
|
|
@@ -15135,9 +14933,7 @@ const handleRequest = async (req, res) => {
|
|
|
15135
14933
|
// scheme the native companion scanners accept; it carries the
|
|
15136
14934
|
// credential encoding because those scanners cannot take a typed code.
|
|
15137
14935
|
const serverName = environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED }).label;
|
|
15138
|
-
const invite = base
|
|
15139
|
-
? `openmausbot://pair?address=${encodeURIComponent(base)}&token=${encodeURIComponent(opened.credential)}&name=${encodeURIComponent(serverName)}`
|
|
15140
|
-
: null;
|
|
14936
|
+
const invite = base ? phonePairingLink({ address: base, token: opened.credential, name: serverName }) : null;
|
|
15141
14937
|
return json(res, 200, {
|
|
15142
14938
|
id: opened.id,
|
|
15143
14939
|
code,
|
|
@@ -15401,6 +15197,9 @@ const handleRequest = async (req, res) => {
|
|
|
15401
15197
|
throw Object.assign(new Error("the internal turn capability has expired"), { status: 401 });
|
|
15402
15198
|
}
|
|
15403
15199
|
};
|
|
15200
|
+
const delegatedThisTurn = (taskId) => {
|
|
15201
|
+
(internalCapability.delegatedThisTurn ??= new Set()).add(taskId);
|
|
15202
|
+
};
|
|
15404
15203
|
// Where an entry came from, as the person will read it in MEMORY.md:
|
|
15405
15204
|
// the room or the thread title, never a bare id unless nothing else
|
|
15406
15205
|
// names the conversation.
|
|
@@ -16057,39 +15856,6 @@ const handleRequest = async (req, res) => {
|
|
|
16057
15856
|
});
|
|
16058
15857
|
return json(res, 201, proposed);
|
|
16059
15858
|
}
|
|
16060
|
-
if (method === "POST" && path === "/api/internal/tightening-requests") {
|
|
16061
|
-
const parsed = z.object({
|
|
16062
|
-
fromBotId: z.string().min(1).max(128),
|
|
16063
|
-
fromThreadId: z.string().min(1).max(128),
|
|
16064
|
-
forBotId: z.string().max(128).optional(),
|
|
16065
|
-
intents: z.unknown(),
|
|
16066
|
-
reason: z.unknown(),
|
|
16067
|
-
}).strict().safeParse(await readInternalBody());
|
|
16068
|
-
if (!parsed.success)
|
|
16069
|
-
return json(res, 400, { error: "invalid tightening proposal" });
|
|
16070
|
-
const body = parsed.data;
|
|
16071
|
-
const from = store.bot(body.fromBotId);
|
|
16072
|
-
if (!from)
|
|
16073
|
-
return json(res, 403, { error: "unknown sender" });
|
|
16074
|
-
const owner = connectorThread(from.id, body.fromThreadId);
|
|
16075
|
-
if (!owner)
|
|
16076
|
-
return json(res, 403, { error: "source conversation does not belong to sender" });
|
|
16077
|
-
const targetBotId = body.forBotId?.trim() || from.id;
|
|
16078
|
-
const proposed = tighteningRequests.submit({
|
|
16079
|
-
botId: from.id,
|
|
16080
|
-
threadId: body.fromThreadId,
|
|
16081
|
-
targetBotId,
|
|
16082
|
-
intents: body.intents,
|
|
16083
|
-
reason: body.reason,
|
|
16084
|
-
from: owner.group ? { botId: from.id, name: from.name, color: from.color } : undefined,
|
|
16085
|
-
});
|
|
16086
|
-
appendDecision(DATA_DIR, {
|
|
16087
|
-
threadId: body.fromThreadId, requestId: proposed.requestId, botId: from.id, botName: from.name,
|
|
16088
|
-
tool: "tighten_permissions", summary: proposed.detail, decision: proposed.state === "applied" ? "auto-approved" : "card-shown",
|
|
16089
|
-
source: proposed.state === "applied" ? "full-access" : "tightening",
|
|
16090
|
-
});
|
|
16091
|
-
return json(res, 201, proposed);
|
|
16092
|
-
}
|
|
16093
15859
|
if (method === "GET" && path === "/api/internal/team-setup-catalog") {
|
|
16094
15860
|
let chief = store.bot(internalCapability.botId);
|
|
16095
15861
|
if (!chief.chiefOfStaff || chief.hidden)
|
|
@@ -16442,6 +16208,7 @@ const handleRequest = async (req, res) => {
|
|
|
16442
16208
|
if (internalCapability.generation === EXTERNAL_RUNTIME_GENERATION && !threadBusy(from.id, fromThreadId)) {
|
|
16443
16209
|
drainThreadDelegations(fromThreadId);
|
|
16444
16210
|
}
|
|
16211
|
+
delegatedThisTurn(queued.id);
|
|
16445
16212
|
return json(res, 200, { busy: true, taskId: queued.id, toBotName: target.name, receipt: peerDeliveryReceipt({
|
|
16446
16213
|
botId: toBotId, botName: target.name, outcome: "queued", taskId: queued.id,
|
|
16447
16214
|
detail: "the teammate was busy; the ask was queued as a delegation instead",
|
|
@@ -16601,6 +16368,7 @@ const handleRequest = async (req, res) => {
|
|
|
16601
16368
|
kind: "activity",
|
|
16602
16369
|
tool: { name: `@${currentTarget.name} is still working — ask converted to a delegation` },
|
|
16603
16370
|
});
|
|
16371
|
+
delegatedThisTurn(taskId);
|
|
16604
16372
|
return json(res, 200, { timeout: true, taskId, toBotName: currentTarget.name, waitedMs: ASK_BOT_TIMEOUT_MS,
|
|
16605
16373
|
receipt: peerDeliveryReceipt({ botId: toBotId, botName: currentTarget.name, outcome: "injected", taskId,
|
|
16606
16374
|
detail: "the teammate's turn started and is still running; only the synchronous wait ended" }) });
|
|
@@ -16639,6 +16407,9 @@ const handleRequest = async (req, res) => {
|
|
|
16639
16407
|
const delegationMatch = method === "GET" ? path.match(/^\/api\/internal\/delegations\/([\w-]{4,64})$/) : null;
|
|
16640
16408
|
if (delegationMatch) {
|
|
16641
16409
|
const taskId = delegationMatch[1];
|
|
16410
|
+
if (internalCapability.delegatedThisTurn?.has(taskId)) {
|
|
16411
|
+
return json(res, 409, { error: `Task ${taskId} was delegated during this turn. Finish your response now so the other bot can work; its result will be delivered to this conversation automatically. Do not check or wait for a newly delegated task until a later turn.` });
|
|
16412
|
+
}
|
|
16642
16413
|
const fromThreadId = internalCapability.threadId;
|
|
16643
16414
|
const from = internalSender;
|
|
16644
16415
|
if (!connectorThread(from.id, fromThreadId))
|
|
@@ -16680,7 +16451,10 @@ const handleRequest = async (req, res) => {
|
|
|
16680
16451
|
if (!sender || (target && !canReachPeer(sender, target))) {
|
|
16681
16452
|
return json(res, 403, { error: "Result withheld: team access changed while the teammate was working" });
|
|
16682
16453
|
}
|
|
16683
|
-
|
|
16454
|
+
// Nothing moves while the teammate's card waits on the person, so
|
|
16455
|
+
// say so now rather than holding the long-poll to its deadline.
|
|
16456
|
+
const awaitingPerson = running && runningEntry ? openPersonCard(runningEntry[0]) : undefined;
|
|
16457
|
+
if (Date.now() >= deadline || awaitingPerson) {
|
|
16684
16458
|
if (running && runningEntry) {
|
|
16685
16459
|
const recent = summarizeDelegatedActivity(store.messagesFor(runningEntry[0]), running.startedAtMs ?? Date.now());
|
|
16686
16460
|
return json(res, 200, {
|
|
@@ -16688,6 +16462,12 @@ const handleRequest = async (req, res) => {
|
|
|
16688
16462
|
toBotName: store.bot(toBotId)?.name ?? toBotId,
|
|
16689
16463
|
elapsedMs: Math.max(0, Date.now() - (running.startedAtMs ?? Date.now())),
|
|
16690
16464
|
recentActivity: recent,
|
|
16465
|
+
...(awaitingPerson ? {
|
|
16466
|
+
awaitingPerson: {
|
|
16467
|
+
...awaitingPerson,
|
|
16468
|
+
threadTitle: store.taskByThread(toBotId, runningEntry[0])?.title,
|
|
16469
|
+
},
|
|
16470
|
+
} : {}),
|
|
16691
16471
|
});
|
|
16692
16472
|
}
|
|
16693
16473
|
const queuedTarget = store.bot(toBotId);
|
|
@@ -16804,6 +16584,7 @@ const handleRequest = async (req, res) => {
|
|
|
16804
16584
|
detail: `${refusal} — nothing was queued`,
|
|
16805
16585
|
}) });
|
|
16806
16586
|
}
|
|
16587
|
+
delegatedThisTurn(queued.id);
|
|
16807
16588
|
const targetName = store.bot(toBotId)?.name ?? toBotId;
|
|
16808
16589
|
// The queue normally drains when the source thread's turn completes. A
|
|
16809
16590
|
// caller this server did not spawn (an external runtime) has no live
|
|
@@ -17032,6 +16813,12 @@ const handleRequest = async (req, res) => {
|
|
|
17032
16813
|
if (!groupId || !message) {
|
|
17033
16814
|
return json(res, 400, { error: "post_to_room needs group_id (from list_rooms) and message" });
|
|
17034
16815
|
}
|
|
16816
|
+
// One turn's worth of posts is a handful; past it the turn has
|
|
16817
|
+
// stopped reporting and started broadcasting. Counted per turn, on
|
|
16818
|
+
// top of the room's own budget below.
|
|
16819
|
+
if ((internalCapability.roomPosts ?? 0) >= MAX_ROOM_POSTS_PER_TURN) {
|
|
16820
|
+
return json(res, 429, { error: `You have already posted ${MAX_ROOM_POSTS_PER_TURN} times this turn, which is the limit. Do not retry — finish your turn and say anything further to the user directly.` });
|
|
16821
|
+
}
|
|
17035
16822
|
if (message.length > ROOM_POST_MAX_CHARS) {
|
|
17036
16823
|
return json(res, 400, {
|
|
17037
16824
|
error: `a room post is at most ${ROOM_POST_MAX_CHARS} characters — post the short version and keep the detail in your own reply`,
|
|
@@ -17160,6 +16947,7 @@ const handleRequest = async (req, res) => {
|
|
|
17160
16947
|
peerPost: unattended ? { unattended: true } : {},
|
|
17161
16948
|
...(attachedVoiceNote ? { attachments: stagedAttachments } : {}),
|
|
17162
16949
|
});
|
|
16950
|
+
internalCapability.roomPosts = (internalCapability.roomPosts ?? 0) + 1;
|
|
17163
16951
|
detachParkedAudio?.();
|
|
17164
16952
|
store.patchGroup(room.id, { unread: true });
|
|
17165
16953
|
// The same visibility contract the peer tools keep: whatever a bot
|
|
@@ -17203,7 +16991,7 @@ const handleRequest = async (req, res) => {
|
|
|
17203
16991
|
if (!fitsOnOneLine(title))
|
|
17204
16992
|
return json(res, 400, { error: "title must fit on one line" });
|
|
17205
16993
|
if (internalCapability.openedThreads >= MAX_THREADS_OPENED_PER_TURN) {
|
|
17206
|
-
return json(res, 429, { error: `
|
|
16994
|
+
return json(res, 429, { error: `You have already opened ${MAX_THREADS_OPENED_PER_TURN} threads this turn, which is the limit. Do not retry — finish your turn and tell the person which threads you still wanted to open, so they can open them or ask you again.` });
|
|
17207
16995
|
}
|
|
17208
16996
|
const toBotId = typeof body.toBotId === "string" && body.toBotId.trim() ? body.toBotId.trim() : from.id;
|
|
17209
16997
|
const target = store.bot(toBotId);
|
|
@@ -17302,6 +17090,7 @@ const handleRequest = async (req, res) => {
|
|
|
17302
17090
|
}
|
|
17303
17091
|
store.setTaskOpenedBy(target.id, task.threadId, { botId: from.id, name: from.name, delegationId: queued.id, at: task.openedBy?.at ?? Date.now() });
|
|
17304
17092
|
internalCapability.openedThreads += 1;
|
|
17093
|
+
delegatedThisTurn(queued.id);
|
|
17305
17094
|
// An honest forecast, not a promise: the handoff starts when this
|
|
17306
17095
|
// turn ends, and by then the target's slots are taken by whatever is
|
|
17307
17096
|
// running there plus the threads this turn already opened ahead.
|
|
@@ -17331,8 +17120,8 @@ const handleRequest = async (req, res) => {
|
|
|
17331
17120
|
if (!chief.chiefOfStaff) {
|
|
17332
17121
|
return json(res, 403, { error: "only a section's Chief of Staff can create operator bots" });
|
|
17333
17122
|
}
|
|
17334
|
-
if (internalCapability.createdBots >=
|
|
17335
|
-
return json(res, 429, { error:
|
|
17123
|
+
if (internalCapability.createdBots >= MAX_CREATED_BOTS_PER_TURN) {
|
|
17124
|
+
return json(res, 429, { error: `You can create at most ${MAX_CREATED_BOTS_PER_TURN} bots in one turn. Use the team you have before adding more.` });
|
|
17336
17125
|
}
|
|
17337
17126
|
if (store.bots.length >= MAX_WORKSPACE_BOTS) {
|
|
17338
17127
|
return json(res, 409, { error: `this workspace is limited to ${MAX_WORKSPACE_BOTS} bots` });
|
|
@@ -17381,8 +17170,9 @@ const handleRequest = async (req, res) => {
|
|
|
17381
17170
|
if (store.bot(chief.id) !== chief || chief.hidden || !chief.chiefOfStaff || !connectorThread(chief.id, fromThreadId)) {
|
|
17382
17171
|
return json(res, 403, { error: "only an active Chief of Staff can create operator bots" });
|
|
17383
17172
|
}
|
|
17384
|
-
if (internalCapability.createdBots >=
|
|
17385
|
-
return json(res, 429, { error:
|
|
17173
|
+
if (internalCapability.createdBots >= MAX_CREATED_BOTS_PER_TURN) {
|
|
17174
|
+
return json(res, 429, { error: `You can create at most ${MAX_CREATED_BOTS_PER_TURN} bots in one turn. Use the team you have before adding more.` });
|
|
17175
|
+
}
|
|
17386
17176
|
if (store.bots.length >= MAX_WORKSPACE_BOTS)
|
|
17387
17177
|
return json(res, 409, { error: `this workspace is limited to ${MAX_WORKSPACE_BOTS} bots` });
|
|
17388
17178
|
const duplicate = store.bots.find((candidate) => !candidate.hidden &&
|
|
@@ -17501,6 +17291,36 @@ const handleRequest = async (req, res) => {
|
|
|
17501
17291
|
const updated = updateChannel(room.id, patch);
|
|
17502
17292
|
return json(res, 200, { ok: true, memberIds: updated.memberIds, memberCount: updated.memberIds.length, message: `Updated room “${updated.name}”.` });
|
|
17503
17293
|
}
|
|
17294
|
+
if (method === "POST" && path === "/api/internal/mcp-servers") {
|
|
17295
|
+
// Same single-flight lock as POST /api/mcp/servers: two writers must
|
|
17296
|
+
// not interleave a read of cfg.mcpServers with a save.
|
|
17297
|
+
if (mcpConfigBusy)
|
|
17298
|
+
return json(res, 409, { error: "MCP servers are already being updated." });
|
|
17299
|
+
mcpConfigBusy = true;
|
|
17300
|
+
try {
|
|
17301
|
+
const body = await readInternalBody();
|
|
17302
|
+
const added = addDisabledMcpServer(cfg.mcpServers ?? {}, body);
|
|
17303
|
+
if (!added.ok)
|
|
17304
|
+
return json(res, added.status, { error: added.error });
|
|
17305
|
+
const refusal = mcpPolicyRefusal(added.name, added.server);
|
|
17306
|
+
if (refusal)
|
|
17307
|
+
return json(res, 403, { error: refusal, code: "managed_policy" });
|
|
17308
|
+
persistMcpServers(added.next);
|
|
17309
|
+
const server = added.server;
|
|
17310
|
+
const remote = isRemoteMcpServer(server);
|
|
17311
|
+
return json(res, 201, {
|
|
17312
|
+
name: added.name,
|
|
17313
|
+
enabled: false,
|
|
17314
|
+
transport: remote ? server.type : "command",
|
|
17315
|
+
target: remote ? server.url : server.command,
|
|
17316
|
+
envKeys: remote ? [] : Object.keys(server.env).sort(),
|
|
17317
|
+
headerKeys: remote ? Object.keys(server.headers).sort() : [],
|
|
17318
|
+
});
|
|
17319
|
+
}
|
|
17320
|
+
finally {
|
|
17321
|
+
mcpConfigBusy = false;
|
|
17322
|
+
}
|
|
17323
|
+
}
|
|
17504
17324
|
if (method === "POST" && path === "/api/internal/request-credential") {
|
|
17505
17325
|
const body = await readInternalBody();
|
|
17506
17326
|
const from = internalSender;
|
|
@@ -18353,12 +18173,10 @@ const handleRequest = async (req, res) => {
|
|
|
18353
18173
|
const limit = pageSize(url.searchParams.get("messages"));
|
|
18354
18174
|
if (limit === null)
|
|
18355
18175
|
return json(res, 400, { error: "messages must be a non-negative whole number" });
|
|
18356
|
-
// wireBot(), not publicBot():
|
|
18357
|
-
//
|
|
18358
|
-
//
|
|
18359
|
-
//
|
|
18360
|
-
// that messagePage() does not: wireBot() omits the key entirely for a
|
|
18361
|
-
// bot record carrying no tasks, where publicBot() always sent [].
|
|
18176
|
+
// wireBot(), not publicBot(): this snapshot answers with the page it
|
|
18177
|
+
// was asked for, not the default one. tasks stays explicit, as in
|
|
18178
|
+
// publicBot(): wireBot() omits the key entirely for a bot record
|
|
18179
|
+
// carrying no tasks, where clients expect [].
|
|
18362
18180
|
// A member on a workspace with a restricted bot gets only what they may
|
|
18363
18181
|
// see (bot-visibility.ts); for everyone else these filters keep all.
|
|
18364
18182
|
const shownBots = store.bots.filter((bot) => visible.bot(bot.id));
|
|
@@ -18382,47 +18200,6 @@ const handleRequest = async (req, res) => {
|
|
|
18382
18200
|
});
|
|
18383
18201
|
}
|
|
18384
18202
|
// scrollback: the page before a message the client already holds
|
|
18385
|
-
// #1655: the consent card's answer, per conversation. Explicit and
|
|
18386
|
-
// revocable; a thread allowlisted in config carries the same standing
|
|
18387
|
-
// consent without ever seeing a card.
|
|
18388
|
-
m = path.match(/^\/api\/threads\/([\w-]+)\/cloud-overflow$/);
|
|
18389
|
-
if (m && method === "POST") {
|
|
18390
|
-
const threadId = m[1];
|
|
18391
|
-
if (!store.botByThread(threadId))
|
|
18392
|
-
return json(res, 404, { error: "no such conversation" });
|
|
18393
|
-
const body = await readBody(req);
|
|
18394
|
-
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
18395
|
-
return json(res, 400, { error: "body must be a JSON object" });
|
|
18396
|
-
if (typeof body.consent !== "boolean")
|
|
18397
|
-
return json(res, 400, { error: "consent must be a boolean" });
|
|
18398
|
-
if (body.consent) {
|
|
18399
|
-
// Consent answers the priced card, never a guess: without a prior
|
|
18400
|
-
// offer the rate was never shown, so a seat must not start yet.
|
|
18401
|
-
// The grant binds to the rate the card showed (#1655): a config
|
|
18402
|
-
// change re-offers instead of starting at a new price silently.
|
|
18403
|
-
const offeredRate = cloudOverflowConsent.offeredRate(threadId);
|
|
18404
|
-
if (offeredRate === null)
|
|
18405
|
-
return json(res, 409, { error: "no cloud overflow offer" });
|
|
18406
|
-
cloudOverflowConsent.grant(threadId, offeredRate);
|
|
18407
|
-
}
|
|
18408
|
-
else {
|
|
18409
|
-
cloudOverflowConsent.revoke(threadId);
|
|
18410
|
-
// Revocation must reach the machine (#1655): the ledger alone
|
|
18411
|
-
// would leave this conversation's seat billing to its idle
|
|
18412
|
-
// timeout. A start still waking is caught by its own revoke
|
|
18413
|
-
// check once readyBox resolves.
|
|
18414
|
-
const seatBot = store.botByThread(threadId);
|
|
18415
|
-
const lease = seatBot ? cloudSeatLeases.get(seatBot.id) : undefined;
|
|
18416
|
-
if (seatBot && lease && lease.threadId === threadId) {
|
|
18417
|
-
stopCloudSeat(seatBot.id, lease, "Cloud computer stopped because overflow consent was revoked — billing pauses while it sleeps.");
|
|
18418
|
-
}
|
|
18419
|
-
}
|
|
18420
|
-
store.appendMessage(threadId, {
|
|
18421
|
-
role: "bot", kind: "activity",
|
|
18422
|
-
tool: { name: cloudOverflowConsentText(body.consent, cloudOverflowIdleStopMs(cfg)), ok: true },
|
|
18423
|
-
});
|
|
18424
|
-
return json(res, 200, { consented: body.consent });
|
|
18425
|
-
}
|
|
18426
18203
|
m = path.match(/^\/api\/threads\/([\w-]+)\/messages$/);
|
|
18427
18204
|
if (m && method === "GET") {
|
|
18428
18205
|
const threadId = m[1];
|
|
@@ -19172,8 +18949,11 @@ const handleRequest = async (req, res) => {
|
|
|
19172
18949
|
return json(res, 400, { error: "Import backups alongside your existing bots; project mode is only for templates" });
|
|
19173
18950
|
try {
|
|
19174
18951
|
const imported = importTeamBackup(store, routines, body, await defaultSelection(), { visibility: importVisibility });
|
|
18952
|
+
// A restore writes its transcripts without store events, after the
|
|
18953
|
+
// store's own frames announced each bot and room, so these frames
|
|
18954
|
+
// are what carries the restored pages to other clients.
|
|
19175
18955
|
const bots = imported.bots.map((bot) => publicBot(bot));
|
|
19176
|
-
const groups = imported.groups.map((group) => (
|
|
18956
|
+
const groups = imported.groups.map((group) => groupWithThread(group));
|
|
19177
18957
|
for (const bot of bots)
|
|
19178
18958
|
broadcast({ kind: "bot", bot });
|
|
19179
18959
|
for (const group of groups)
|
|
@@ -19323,9 +19103,7 @@ const handleRequest = async (req, res) => {
|
|
|
19323
19103
|
// Who opened it decides who may answer its cards on a shared workspace.
|
|
19324
19104
|
if (auth.kind === "session")
|
|
19325
19105
|
threadStarters.set(task.threadId, actorKey(auth));
|
|
19326
|
-
|
|
19327
|
-
broadcast({ kind: "group", group: fresh });
|
|
19328
|
-
return json(res, 201, { group: fresh, task });
|
|
19106
|
+
return json(res, 201, { group: groupWithThread(store.group(group.id)), task });
|
|
19329
19107
|
}
|
|
19330
19108
|
m = path.match(/^\/api\/groups\/([\w-]+)\/tasks\/([\w-]+)$/);
|
|
19331
19109
|
if (m && method === "POST") {
|
|
@@ -19350,15 +19128,6 @@ const handleRequest = async (req, res) => {
|
|
|
19350
19128
|
if (!switched)
|
|
19351
19129
|
return json(res, 404, { error: "no such channel task" });
|
|
19352
19130
|
const switchedSettings = { ...publicGroupState(switched), tasks: store.groupTasks(switched.id) };
|
|
19353
|
-
// The frame says the channel moved; it is not a transcript delivery.
|
|
19354
|
-
// A long room's whole history over SSE is the payload `?messages=`
|
|
19355
|
-
// exists to avoid, and it would also overwrite a bounded snapshot on
|
|
19356
|
-
// every other client. One default page is enough to render the switch;
|
|
19357
|
-
// anything earlier pages back through /api/threads/:id/messages.
|
|
19358
|
-
broadcast({
|
|
19359
|
-
kind: "group",
|
|
19360
|
-
group: { ...switchedSettings, ...messagePage(switched.threadId, DEFAULT_PAGE) },
|
|
19361
|
-
});
|
|
19362
19131
|
// "0" predates paging and means settings only — no `messages` key at
|
|
19363
19132
|
// all, which clients tell apart from an empty page. A positive page is
|
|
19364
19133
|
// the transcript a client can actually hold; omitting the parameter
|
|
@@ -19366,9 +19135,7 @@ const handleRequest = async (req, res) => {
|
|
|
19366
19135
|
// materialises it.
|
|
19367
19136
|
const responseGroup = requestedMessages === "0"
|
|
19368
19137
|
? switchedSettings
|
|
19369
|
-
: switchLimit
|
|
19370
|
-
? groupWithThread(switched)
|
|
19371
|
-
: { ...switchedSettings, ...messagePage(switched.threadId, switchLimit) };
|
|
19138
|
+
: { ...switchedSettings, ...messagePage(switched.threadId, switchLimit) };
|
|
19372
19139
|
return json(res, 200, { group: responseGroup });
|
|
19373
19140
|
}
|
|
19374
19141
|
if (m && method === "PATCH") {
|
|
@@ -19441,14 +19208,12 @@ const handleRequest = async (req, res) => {
|
|
|
19441
19208
|
lastReply.delete(m[2]);
|
|
19442
19209
|
cancelTeamSetupResumesForThread(m[2]);
|
|
19443
19210
|
const updated = store.deleteGroupTask(group.id, m[2]);
|
|
19444
|
-
if (updated)
|
|
19445
|
-
clearTurnDigestState(m[2]);
|
|
19446
19211
|
if (!updated)
|
|
19447
19212
|
return json(res, 400, { error: "a channel keeps at least one task" });
|
|
19213
|
+
clearTurnDigestState(m[2]);
|
|
19214
|
+
revokeInternalCapabilitiesForThread(m[2]);
|
|
19448
19215
|
rejectDeletedThreadSkillStages(stagedSkillCleanups);
|
|
19449
|
-
|
|
19450
|
-
broadcast({ kind: "group", group: fresh });
|
|
19451
|
-
return json(res, 200, { group: fresh });
|
|
19216
|
+
return json(res, 200, { group: groupWithThread(updated) });
|
|
19452
19217
|
}
|
|
19453
19218
|
m = path.match(/^\/api\/groups\/([\w-]+)$/);
|
|
19454
19219
|
if (m && method === "PATCH") {
|
|
@@ -21412,54 +21177,6 @@ const handleRequest = async (req, res) => {
|
|
|
21412
21177
|
broadcast({ kind: "bot", bot: visible });
|
|
21413
21178
|
return json(res, 200, { bot: visible });
|
|
21414
21179
|
}
|
|
21415
|
-
// ── workspace checkpoints: per-turn shadow-git snapshots ────────────
|
|
21416
|
-
// The list endpoint is the source of truth (turns store nothing), and
|
|
21417
|
-
// `enabled` tells the UI whether snapshots can happen here at all —
|
|
21418
|
-
// false for refused folders (home, Desktop…), a missing git, or a bot
|
|
21419
|
-
// whose checkpoints failed earlier this session.
|
|
21420
|
-
m = path.match(/^\/api\/bots\/([\w-]+)\/checkpoints$/);
|
|
21421
|
-
if (m && method === "GET") {
|
|
21422
|
-
if (!store.bot(m[1]))
|
|
21423
|
-
return json(res, 404, { error: "no such bot" });
|
|
21424
|
-
const cwd = url.searchParams.get("cwd") ?? "";
|
|
21425
|
-
if (!cwd.trim())
|
|
21426
|
-
return json(res, 400, { error: "cwd query parameter required" });
|
|
21427
|
-
return json(res, 200, {
|
|
21428
|
-
checkpoints: await checkpoints.listCheckpoints(m[1], cwd),
|
|
21429
|
-
enabled: await checkpoints.checkpointsEnabled(m[1], cwd),
|
|
21430
|
-
});
|
|
21431
|
-
}
|
|
21432
|
-
m = path.match(/^\/api\/bots\/([\w-]+)\/checkpoints\/restore$/);
|
|
21433
|
-
if (m && method === "POST") {
|
|
21434
|
-
const bot = store.bot(m[1]);
|
|
21435
|
-
if (!bot)
|
|
21436
|
-
return json(res, 404, { error: "no such bot" });
|
|
21437
|
-
const parsed = z
|
|
21438
|
-
.object({ cwd: z.string().min(1), hash: z.string().regex(/^[0-9a-f]{40}$/) })
|
|
21439
|
-
.safeParse(await readBody(req));
|
|
21440
|
-
if (!parsed.success) {
|
|
21441
|
-
return json(res, 400, { error: "cwd (absolute path) and hash (full 40-character checkpoint hash) required" });
|
|
21442
|
-
}
|
|
21443
|
-
// Claim synchronously with the busy check. startTurn checks the same
|
|
21444
|
-
// lease before reserving the bot, so no turn can enter during the
|
|
21445
|
-
// awaited Git operation.
|
|
21446
|
-
if (bot.busy)
|
|
21447
|
-
return json(res, 409, { error: "the bot is working — stop the turn before restoring files" });
|
|
21448
|
-
if (checkpointRestoreLeases.has(bot.id)) {
|
|
21449
|
-
return json(res, 409, { error: "this bot's project files are already being restored" });
|
|
21450
|
-
}
|
|
21451
|
-
checkpointRestoreLeases.add(bot.id);
|
|
21452
|
-
let result;
|
|
21453
|
-
try {
|
|
21454
|
-
result = await checkpoints.restore(bot.id, parsed.data.cwd, parsed.data.hash);
|
|
21455
|
-
}
|
|
21456
|
-
finally {
|
|
21457
|
-
checkpointRestoreLeases.delete(bot.id);
|
|
21458
|
-
}
|
|
21459
|
-
if (!result.ok)
|
|
21460
|
-
return json(res, 400, { error: result.error });
|
|
21461
|
-
return json(res, 200, { ok: true });
|
|
21462
|
-
}
|
|
21463
21180
|
// onboarding/ask cards persist their answered/dismissed state
|
|
21464
21181
|
m = path.match(/^\/api\/bots\/([\w-]+)\/cards\/([\w-]+)$/);
|
|
21465
21182
|
if (m && method === "PATCH") {
|
|
@@ -21863,14 +21580,6 @@ const handleRequest = async (req, res) => {
|
|
|
21863
21580
|
behavior,
|
|
21864
21581
|
}))
|
|
21865
21582
|
return;
|
|
21866
|
-
if (resolveAndSendTightening(res, {
|
|
21867
|
-
botId: bot.id,
|
|
21868
|
-
botName: bot.name,
|
|
21869
|
-
threadId: bot.threadId,
|
|
21870
|
-
requestId: String(body.requestId),
|
|
21871
|
-
behavior,
|
|
21872
|
-
}))
|
|
21873
|
-
return;
|
|
21874
21583
|
if (sendSkillResolution(res, resolveSkillRequest({
|
|
21875
21584
|
botId: bot.id,
|
|
21876
21585
|
botName: bot.name,
|
|
@@ -22011,21 +21720,6 @@ const handleRequest = async (req, res) => {
|
|
|
22011
21720
|
}))
|
|
22012
21721
|
return;
|
|
22013
21722
|
}
|
|
22014
|
-
const tighteningCard = store.messagesFor(threadId).find((message) => message.card?.requestId === requestId && message.card.tighteningRequest);
|
|
22015
|
-
if (tighteningCard?.card?.tighteningRequest) {
|
|
22016
|
-
const tighteningBotId = tighteningCard.from?.botId ?? store.botByThread(threadId)?.id;
|
|
22017
|
-
if (!tighteningBotId)
|
|
22018
|
-
return json(res, 400, { error: "this tightening request has no valid owner" });
|
|
22019
|
-
const tighteningOwner = store.bot(tighteningBotId);
|
|
22020
|
-
if (resolveAndSendTightening(res, {
|
|
22021
|
-
botId: tighteningBotId,
|
|
22022
|
-
botName: tighteningOwner?.name,
|
|
22023
|
-
threadId,
|
|
22024
|
-
requestId,
|
|
22025
|
-
behavior,
|
|
22026
|
-
}))
|
|
22027
|
-
return;
|
|
22028
|
-
}
|
|
22029
21723
|
// peer-approval intercept (see /api/bots/:id/respond above). A peer card
|
|
22030
21724
|
// belongs to the bus rather than to a speaker, so resolve it before we go
|
|
22031
21725
|
// looking for one — a room between turns has no speaker to find.
|
|
@@ -22148,15 +21842,8 @@ const handleRequest = async (req, res) => {
|
|
|
22148
21842
|
return json(res, 200, { ok: true });
|
|
22149
21843
|
}
|
|
22150
21844
|
// ── tasks: a bot's separate contexts ────────────────────────────────
|
|
22151
|
-
//
|
|
22152
|
-
//
|
|
22153
|
-
// the client showing the previous task's conversation.
|
|
22154
|
-
const botWithThread = (bot) => ({
|
|
22155
|
-
...wireBot(bot),
|
|
22156
|
-
messages: store.messagesFor(bot.threadId),
|
|
22157
|
-
activeLeafId: store.activeLeaf(bot.threadId),
|
|
22158
|
-
tasks: store.tasks(bot.id).map(wireTask),
|
|
22159
|
-
});
|
|
21845
|
+
// Replies carry the open thread's newest page: a create or delete can
|
|
21846
|
+
// change which transcript is live. The frames come from store.onChange.
|
|
22160
21847
|
// Folders organize one bot's threads; they never own settings,
|
|
22161
21848
|
// transcripts or working directories. The project wire names stay stable.
|
|
22162
21849
|
m = path.match(/^\/api\/bots\/([\w-]+)\/projects\/order$/);
|
|
@@ -22173,7 +21860,7 @@ const handleRequest = async (req, res) => {
|
|
|
22173
21860
|
const projects = store.reorderProjects(bot.id, body.projectIds);
|
|
22174
21861
|
if (!projects)
|
|
22175
21862
|
return json(res, 400, { error: "projectIds must include each of this bot's folders exactly once" });
|
|
22176
|
-
return json(res, 200, { projects, bot:
|
|
21863
|
+
return json(res, 200, { projects, bot: publicBot(bot) });
|
|
22177
21864
|
}
|
|
22178
21865
|
m = path.match(/^\/api\/bots\/([\w-]+)\/projects(?:\/([\w-]+))?$/);
|
|
22179
21866
|
if (m && ((method === "POST" && !m[2]) || (method === "PATCH" && m[2]) || (method === "DELETE" && m[2]))) {
|
|
@@ -22184,7 +21871,7 @@ const handleRequest = async (req, res) => {
|
|
|
22184
21871
|
return json(res, 404, { error: "no such folder" });
|
|
22185
21872
|
if (method === "DELETE") {
|
|
22186
21873
|
const updated = store.deleteProject(bot.id, m[2]);
|
|
22187
|
-
return json(res, 200, { bot:
|
|
21874
|
+
return json(res, 200, { bot: publicBot(updated) });
|
|
22188
21875
|
}
|
|
22189
21876
|
const body = await readBody(req);
|
|
22190
21877
|
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
@@ -22207,7 +21894,7 @@ const handleRequest = async (req, res) => {
|
|
|
22207
21894
|
const project = method === "POST"
|
|
22208
21895
|
? store.createProject(bot.id, patch.name, patch.emoji)
|
|
22209
21896
|
: store.patchProject(bot.id, m[2], patch);
|
|
22210
|
-
return json(res, method === "POST" ? 201 : 200, { project, bot:
|
|
21897
|
+
return json(res, method === "POST" ? 201 : 200, { project, bot: publicBot(bot) });
|
|
22211
21898
|
}
|
|
22212
21899
|
m = path.match(/^\/api\/bots\/([\w-]+)\/tasks$/);
|
|
22213
21900
|
if (m && method === "POST") {
|
|
@@ -22242,9 +21929,7 @@ const handleRequest = async (req, res) => {
|
|
|
22242
21929
|
// Who opened it decides who may answer its cards on a shared workspace.
|
|
22243
21930
|
if (auth.kind === "session")
|
|
22244
21931
|
threadStarters.set(task.threadId, actorKey(auth));
|
|
22245
|
-
|
|
22246
|
-
broadcast({ kind: "bot", bot: fresh });
|
|
22247
|
-
return json(res, 201, { bot: fresh, task: wireTask(task) });
|
|
21932
|
+
return json(res, 201, { bot: publicBot(store.bot(bot.id)), task: wireTask(task) });
|
|
22248
21933
|
}
|
|
22249
21934
|
m = path.match(/^\/api\/bots\/([\w-]+)\/tasks\/([\w-]+)$/);
|
|
22250
21935
|
if (m && method === "POST") {
|
|
@@ -22263,18 +21948,13 @@ const handleRequest = async (req, res) => {
|
|
|
22263
21948
|
if (!switched)
|
|
22264
21949
|
return json(res, 404, { error: "no such task" });
|
|
22265
21950
|
const switchedSettings = { ...wireBot(switched), tasks: store.tasks(switched.id).map(wireTask) };
|
|
22266
|
-
// Bounded for the same reason as the channel switch above.
|
|
22267
|
-
broadcast({
|
|
22268
|
-
kind: "bot",
|
|
22269
|
-
bot: { ...switchedSettings, ...messagePage(switched.threadId, DEFAULT_PAGE) },
|
|
22270
|
-
});
|
|
22271
21951
|
// "0" is settings only, a positive page is a bounded transcript, and no
|
|
22272
21952
|
// parameter is the whole thread — the one branch that materialises it.
|
|
21953
|
+
// Search results land through that branch (src/lib/focus-message.ts):
|
|
21954
|
+
// the hit can be older than one page.
|
|
22273
21955
|
const responseBot = requestedMessages === "0"
|
|
22274
21956
|
? switchedSettings
|
|
22275
|
-
: switchLimit
|
|
22276
|
-
? botWithThread(switched)
|
|
22277
|
-
: { ...switchedSettings, ...messagePage(switched.threadId, switchLimit) };
|
|
21957
|
+
: { ...switchedSettings, ...messagePage(switched.threadId, switchLimit) };
|
|
22278
21958
|
return json(res, 200, { bot: responseBot });
|
|
22279
21959
|
}
|
|
22280
21960
|
if (m && method === "PATCH") {
|
|
@@ -22349,9 +22029,7 @@ const handleRequest = async (req, res) => {
|
|
|
22349
22029
|
const task = store.refreshTaskPermissions(profile.id, m[2]);
|
|
22350
22030
|
if (!task)
|
|
22351
22031
|
return json(res, 404, { error: "no such task" });
|
|
22352
|
-
|
|
22353
|
-
broadcast({ kind: "bot", bot: fresh });
|
|
22354
|
-
return json(res, 200, { task: wireTask(task), bot: fresh });
|
|
22032
|
+
return json(res, 200, { task: wireTask(task), bot: publicBot(store.bot(profile.id)) });
|
|
22355
22033
|
}
|
|
22356
22034
|
const patch = {};
|
|
22357
22035
|
if (body.projectId !== undefined) {
|
|
@@ -22457,9 +22135,7 @@ const handleRequest = async (req, res) => {
|
|
|
22457
22135
|
const task = patch.modelSelection
|
|
22458
22136
|
? store.switchTaskModel(m[1], m[2], patch.modelSelection, body.updateBotDefault === true, body.resetApprovalToAsk === true, { ...patch, ...hostedModels?.resetTask(current.modelSelection, patch.modelSelection) })
|
|
22459
22137
|
: store.patchTask(m[1], m[2], patch);
|
|
22460
|
-
|
|
22461
|
-
broadcast({ kind: "bot", bot: fresh });
|
|
22462
|
-
return json(res, 200, { task: wireTask(task), bot: fresh });
|
|
22138
|
+
return json(res, 200, { task: wireTask(task), bot: publicBot(store.bot(m[1])) });
|
|
22463
22139
|
}
|
|
22464
22140
|
if (m && method === "DELETE") {
|
|
22465
22141
|
const bot = store.bot(m[1]);
|
|
@@ -22482,12 +22158,11 @@ const handleRequest = async (req, res) => {
|
|
|
22482
22158
|
if (!updated)
|
|
22483
22159
|
return json(res, 404, { error: "no such task" });
|
|
22484
22160
|
clearTurnDigestState(m[2]);
|
|
22161
|
+
revokeInternalCapabilitiesForThread(m[2]);
|
|
22485
22162
|
handoffs.forget(m[2]);
|
|
22486
22163
|
settleDirectFollowup(directTurnGenerationByThread.get(m[2]));
|
|
22487
22164
|
rejectDeletedThreadSkillStages(stagedSkillCleanups);
|
|
22488
|
-
|
|
22489
|
-
broadcast({ kind: "bot", bot: fresh });
|
|
22490
|
-
return json(res, 200, { bot: fresh });
|
|
22165
|
+
return json(res, 200, { bot: publicBot(updated) });
|
|
22491
22166
|
}
|
|
22492
22167
|
// Regenerate title (#1858): name the thread again from its conversation
|
|
22493
22168
|
// as it is now. The person asked, so the result replaces any title, but
|
|
@@ -24112,6 +23787,15 @@ const handleRequest = async (req, res) => {
|
|
|
24112
23787
|
if (conflict)
|
|
24113
23788
|
return json(res, 409, { error: conflict });
|
|
24114
23789
|
}
|
|
23790
|
+
// A change names only the OpenCode provider keys it saves or removes;
|
|
23791
|
+
// the rest stay. Merged with no await before the save below, so it
|
|
23792
|
+
// starts from what is saved at that moment.
|
|
23793
|
+
if (patch.opencodeGo?.providerKeys) {
|
|
23794
|
+
const merged = mergeOpenCodeProviderKeys(cfg, patch.opencodeGo.providerKeys);
|
|
23795
|
+
if (!merged.ok)
|
|
23796
|
+
return json(res, 400, { error: merged.error });
|
|
23797
|
+
patch.opencodeGo = { ...patch.opencodeGo, providerKeys: merged.keys };
|
|
23798
|
+
}
|
|
24115
23799
|
const browserCleanupRequests = [];
|
|
24116
23800
|
if (profileControlConflict())
|
|
24117
23801
|
return json(res, 409, { error: "Release browser control before deleting its profile." });
|
|
@@ -25050,6 +24734,8 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
25050
24734
|
// the shutdown deadline), immediately before the process exits, so no new
|
|
25051
24735
|
// server can overlap with a still-mutating old one.
|
|
25052
24736
|
exit: (code) => {
|
|
24737
|
+
// Streamed text the bus is still merging reaches the log and clients.
|
|
24738
|
+
bus.flush();
|
|
25053
24739
|
try {
|
|
25054
24740
|
sessions.close();
|
|
25055
24741
|
}
|
|
@@ -25062,8 +24748,8 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
25062
24748
|
closeMessageDb();
|
|
25063
24749
|
mcpOAuth.dispose();
|
|
25064
24750
|
releaseDataDirLeaseAtExit();
|
|
25065
|
-
//
|
|
25066
|
-
process.exit(
|
|
24751
|
+
// Every launcher in this repo starts the server again on this code (server/restart.ts).
|
|
24752
|
+
process.exit(restartRequested && code === 0 ? RESTART_EXIT_CODE : code);
|
|
25067
24753
|
},
|
|
25068
24754
|
});
|
|
25069
24755
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|