openmausbot 0.1.96 → 0.1.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/ActivityPanel-H6D3SD39.js +1 -0
- package/dist/assets/BotSettingsDialog-Dqz_AiBZ.js +7 -0
- package/dist/assets/CloudBackendPicker-iezpJFpF.js +1 -0
- package/dist/assets/ComputerPanel-DZPj-IWu.js +1 -0
- package/dist/assets/DesktopViewer-utONms1p.js +5 -0
- package/dist/assets/InspectorPanel-OVaBVrzC.js +5 -0
- package/dist/assets/KeyboardShortcutsModal-gk9mvHj_.js +1 -0
- package/dist/assets/LocalVmWorkspace-DLIvd2kC.js +1 -0
- package/dist/assets/MacLocalControl-bJv8yiPB.js +1 -0
- package/dist/assets/NewBotDialog-1qGOAdUM.js +2 -0
- package/dist/assets/RemoteAgentSettingsPanel-DqKr0HkO.js +1 -0
- package/dist/assets/RoutinesPage-BM2D4qGi.js +1 -0
- package/dist/assets/RoutinesSection-Dny9ra-c.js +1 -0
- package/dist/assets/SettingsModal-gC6XJJmt.js +11 -0
- package/dist/assets/TeamMapPage-B6fuu4r_.js +8 -0
- package/dist/assets/TriggersPanel-j0qTP2CO.js +1 -0
- package/dist/assets/VisibilitySection-DT0FqPvu.js +12 -0
- package/dist/assets/WebhooksPanel--YQyCjKO.js +3 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
- package/dist/assets/activity-BxOkayvD.js +1 -0
- package/dist/assets/{arc-Duj5Coy2.js → arc-CJNA94c6.js} +1 -1
- package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
- package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
- package/dist/assets/brain-CFZ7PEXc.js +1 -0
- package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
- package/dist/assets/channel-WhFyujhZ.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-DCCyoLpj.js} +1 -1
- package/dist/assets/computer-control-NLov352p.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KyImBKvE.js +1 -0
- package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
- package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
- package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-CsoXr2D7.js} +1 -1
- package/dist/assets/image-plus-BbVjGHin.js +1 -0
- package/dist/assets/index-BrPW5HDO.css +1 -0
- package/dist/assets/index-C3NqfdVU.js +585 -0
- package/dist/assets/{index-DreDnjOu.js → index-CJTp9JEp.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
- package/dist/assets/{linear-Bl669148.js → linear-BVVKbEMy.js} +1 -1
- package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-JJdoKyKB.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-B4aE3T0q.js} +1 -1
- package/dist/assets/module-DCUsy2BQ.js +3 -0
- package/dist/assets/page-visible-BBo_Zm8g.js +1 -0
- package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
- package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
- package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-BkSCBEwp.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CmTirbVT.js +8 -0
- package/dist/assets/table-file.worker-KkMx-SNx.js +10 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
- package/dist/assets/user-DcZBnAuv.js +1 -0
- package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +20 -9
- package/dist-server/container-mcp.js +8 -0
- package/dist-server/desktop-entry.mjs +54 -0
- package/dist-server/drivers/agents-proxy.js +26 -9
- package/dist-server/index.js +3734 -2360
- package/dist-server/openmausbot.js +1664 -1553
- package/dist-server/pair-cli.js +1664 -1553
- package/dist-server/permission-proxy.js +2 -4
- package/dist-server/server/atomic.js +70 -2
- package/dist-server/server/auto-approve.js +1 -1
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +68 -84
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +275 -44
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +4 -4
- package/dist-server/server/cloud-home.js +3 -9
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +29 -12
- package/dist-server/server/connector-card-text.js +15 -0
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/default-model-selection.js +23 -14
- package/dist-server/server/delegations.js +11 -5
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +11 -1
- package/dist-server/server/drivers/acp/grok.js +47 -5
- package/dist-server/server/drivers/acp/opencode-go.js +16 -21
- package/dist-server/server/drivers/agents-call.js +1 -1
- package/dist-server/server/drivers/agents-catalog.js +27 -7
- package/dist-server/server/drivers/agents-proxy.js +1 -0
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
- package/dist-server/server/drivers/claude-login-auth.js +1 -1
- package/dist-server/server/drivers/claude.js +84 -41
- package/dist-server/server/drivers/codex-device-auth.js +27 -319
- package/dist-server/server/drivers/codex.js +2 -2
- package/dist-server/server/drivers/device-auth.js +342 -0
- package/dist-server/server/drivers/openai-chat.js +110 -10
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/fleet-agent.js +18 -5
- package/dist-server/server/harness/http.js +5 -2
- package/dist-server/server/harness/registry.js +19 -0
- package/dist-server/server/harness-capabilities.js +7 -4
- package/dist-server/server/hosted-models.js +3 -1
- package/dist-server/server/index.js +823 -333
- package/dist-server/server/key-rejections.js +57 -0
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/message-file.js +7 -4
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-roster.js +10 -2
- package/dist-server/server/permission-proxy.js +4 -0
- package/dist-server/server/provider-key-check.js +5 -1
- package/dist-server/server/request-auth.js +1 -1
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/room-turn-timeout.js +28 -0
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routes/thread-models.js +36 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +3 -3
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/skills.js +3 -2
- package/dist-server/server/store.js +273 -61
- package/dist-server/server/surface.js +29 -49
- package/dist-server/server/system-prompt.js +10 -10
- package/dist-server/server/team-backup.js +19 -4
- package/dist-server/server/thread-model.js +25 -0
- package/dist-server/server/turn-continuation.js +168 -0
- package/dist-server/server/webhook-ingress.js +8 -3
- package/dist-server/server/webhooks.js +40 -5
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/ask-question.js +0 -70
- package/dist-server/shared/cloud-computer.js +4 -6
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/device-sign-in.js +57 -0
- package/dist-server/shared/failed-turn.js +1 -4
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/shared/thread-model.js +13 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/dist-server/vps-container-mcp.js +8 -0
- package/dist-server/workspace-backup.worker.js +41 -22
- package/package.json +1 -1
- package/dist/assets/DesktopViewer-DsnA23KM.js +0 -5
- package/dist/assets/architectureDiagram-NJMV4G6O-uYUsDawP.js +0 -36
- package/dist/assets/channel-BZxwC-Gw.js +0 -1
- package/dist/assets/cose-bilkent-JH36ORCC-D-Uglxqq.js +0 -1
- package/dist/assets/ganttDiagram-FUAMR5RP-CKV5D2F6.js +0 -292
- package/dist/assets/index-Du7o6Dsf.js +0 -610
- package/dist/assets/index-KxG4bWSr.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
- package/dist-server/server/drivers/boatagent.js +0 -443
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { chmodSync, existsSync, readFileSync, mkdirSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { writeFileAtomic } from "./atomic.js";
|
|
8
|
+
import { writeFileAtomic, writeFileAtomicIfChanged } from "./atomic.js";
|
|
9
9
|
import { parseToolScope, toolScopeWidens } from "../shared/tool-scope.js";
|
|
10
10
|
import { ensureSections, readSections, changeEmptySection } from "./section-context.js";
|
|
11
11
|
import { removeBotFolder, soulFile, soulHash, writeSoulMirror } from "./bot-folder.js";
|
|
@@ -18,9 +18,11 @@ import { newId } from "./contracts.js";
|
|
|
18
18
|
import { pickBotName } from "./names.js";
|
|
19
19
|
import { redactSecretsInText } from "./redact.js";
|
|
20
20
|
import { AVATAR_FOCUS_CENTER, AVATAR_ZOOM_MIN, botAvatarProfile, clampAvatarFocus, clampAvatarZoom } from "../shared/bot-avatar.js";
|
|
21
|
-
import { approvalModeFor, isApprovalMode } from "../shared/approval-mode.js";
|
|
21
|
+
import { approvalModeFor, isApprovalMode, modelSwitchNeedsAsk } from "../shared/approval-mode.js";
|
|
22
|
+
import { REMOVED_COMPUTER_DRIVER } from "./computer-engine-removal.js";
|
|
22
23
|
import { isMentionBoundary, isMentionNameContinuation } from "../shared/mention-boundary.js";
|
|
23
24
|
import { CONNECTOR_SLUG_PATTERN, CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
|
|
25
|
+
import { sameModelSelection } from "../shared/thread-model.js";
|
|
24
26
|
export const groupWireProjectionIsExact = true;
|
|
25
27
|
// Unicode's complete emoji sequences include flags, skin tones and ZWJ
|
|
26
28
|
// combinations. Also allow unqualified single symbols (e.g. ♥), but not
|
|
@@ -31,13 +33,19 @@ export function isProjectEmoji(value) {
|
|
|
31
33
|
}
|
|
32
34
|
export const taskWireProjectionIsExact = true;
|
|
33
35
|
/** The typed wire projection for one task. Pairs with the assertion above:
|
|
34
|
-
* returning WireTask means an undeclared server field cannot ride silently.
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
* returning WireTask means an undeclared server field cannot ride silently.
|
|
37
|
+
* A thread with no model of its own follows its bot: the wire carries the
|
|
38
|
+
* model it runs on either way, so every client reads one effective model. */
|
|
39
|
+
export function toWireTask(task, botModel) {
|
|
40
|
+
const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, surfaceSource, ...stored } = task;
|
|
41
|
+
const wire = { ...stored, modelSelection: structuredClone(task.modelSelection ?? botModel), followsBotModel: task.modelSelection === undefined };
|
|
37
42
|
// Who pinned stays private. A client learns only whether the pin is the
|
|
38
43
|
// machine's own record, so it never presents one as the person's choice.
|
|
39
44
|
return surfaceSource === "auto" && wire.surface !== undefined ? { ...wire, surfaceAuto: true } : wire;
|
|
40
45
|
}
|
|
46
|
+
/** The level a thread drops to when its engine changes under a level the new
|
|
47
|
+
* engine would have to confirm (modelSwitchNeedsAsk), as on every switch. */
|
|
48
|
+
const ASK_NOW = { approvalMode: "ask", autoApprove: false, alwaysAllow: [] };
|
|
41
49
|
const TASK_PATCH_FIELDS = [
|
|
42
50
|
"title", "projectId", "modelSelection", "approvalMode", "autoApprove", "alwaysAllow",
|
|
43
51
|
"unread", "rewound", "archivedAt", "pinned", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
|
|
@@ -325,6 +333,9 @@ export function parseConnectorTools(value) {
|
|
|
325
333
|
}
|
|
326
334
|
const BOTS_FILE = join(DATA_DIR, "bots.json");
|
|
327
335
|
const GROUPS_FILE = join(DATA_DIR, "groups.json");
|
|
336
|
+
/** Written once the copies older builds made of a bot's model on its threads
|
|
337
|
+
* are cleared (Store load), so that cleanup runs once per data directory. */
|
|
338
|
+
const THREAD_MODEL_COPIES_CLEARED = join(DATA_DIR, "thread-model-copies-cleared");
|
|
328
339
|
/** The registries carry souls, project paths and per-bot settings, so they are
|
|
329
340
|
* owner-only like the other data-dir stores. A file written by an older
|
|
330
341
|
* release (or loosened by hand) is tightened on load. Best effort: failing to
|
|
@@ -455,6 +466,7 @@ export class Store {
|
|
|
455
466
|
threads = new Map();
|
|
456
467
|
defaultSelection;
|
|
457
468
|
completeNewBotSelection;
|
|
469
|
+
driverKindOf;
|
|
458
470
|
listeners = new Set();
|
|
459
471
|
/** A broken team registry must not prevent loading independent chat data. */
|
|
460
472
|
registeringInitialSections = true;
|
|
@@ -464,9 +476,14 @@ export class Store {
|
|
|
464
476
|
constructor(defaultSelection,
|
|
465
477
|
/** Workspace-wide new-bot defaults (config newBots), applied to every
|
|
466
478
|
* new bot's selection whichever path created it. */
|
|
467
|
-
completeNewBotSelection = (selection) => selection
|
|
479
|
+
completeNewBotSelection = (selection) => selection,
|
|
480
|
+
/** The driver an engine instance runs, for the level a thread keeps when
|
|
481
|
+
* its engine changes (modelSwitchNeedsAsk). Unknown counts as another
|
|
482
|
+
* driver, so Full and Custom fall back to Ask. */
|
|
483
|
+
driverKindOf = () => undefined) {
|
|
468
484
|
this.defaultSelection = defaultSelection;
|
|
469
485
|
this.completeNewBotSelection = completeNewBotSelection;
|
|
486
|
+
this.driverKindOf = driverKindOf;
|
|
470
487
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
471
488
|
for (const file of [BOTS_FILE, GROUPS_FILE])
|
|
472
489
|
tightenRegistryFile(file);
|
|
@@ -695,11 +712,17 @@ export class Store {
|
|
|
695
712
|
}
|
|
696
713
|
if (groupsMigrated)
|
|
697
714
|
this.saveGroups();
|
|
715
|
+
// Older builds copied a bot's model onto every thread it made, so a
|
|
716
|
+
// thread on exactly its bot's model is a copy, not a person's pick: once
|
|
717
|
+
// per data directory it is cleared and the thread follows its bot. A
|
|
718
|
+
// different model can't be told apart from a pick and stays. Only over a
|
|
719
|
+
// bot list this load could read, so an unreadable file is not skipped.
|
|
720
|
+
const clearModelCopies = !existsSync(THREAD_MODEL_COPIES_CLEARED) && (botsLoaded || !existsSync(BOTS_FILE));
|
|
698
721
|
// bots saved before tasks existed have one endless thread; adopt it as
|
|
699
722
|
// their first task so nothing is lost and nothing special-cases it
|
|
700
723
|
for (const b of this.bots) {
|
|
701
|
-
// Folders are organizational only. Preserve
|
|
702
|
-
//
|
|
724
|
+
// Folders are organizational only. Preserve threads' own models while
|
|
725
|
+
// discarding the unshipped folder-default setting.
|
|
703
726
|
if (b.projects?.some((project) => "modelSelection" in project)) {
|
|
704
727
|
b.projects = b.projects.map(({ id, name, emoji }) => ({ id, name, ...(isProjectEmoji(emoji) ? { emoji } : {}) }));
|
|
705
728
|
botsMigrated = true;
|
|
@@ -729,8 +752,8 @@ export class Store {
|
|
|
729
752
|
botsMigrated = true;
|
|
730
753
|
}
|
|
731
754
|
for (const task of b.tasks) {
|
|
732
|
-
if (task.modelSelection
|
|
733
|
-
task.modelSelection
|
|
755
|
+
if (clearModelCopies && task.modelSelection !== undefined && sameModelSelection(task.modelSelection, b.modelSelection)) {
|
|
756
|
+
delete task.modelSelection;
|
|
734
757
|
botsMigrated = true;
|
|
735
758
|
}
|
|
736
759
|
if (!task.resumeCursors) {
|
|
@@ -773,6 +796,8 @@ export class Store {
|
|
|
773
796
|
}
|
|
774
797
|
if (botsMigrated)
|
|
775
798
|
this.saveBots();
|
|
799
|
+
if (clearModelCopies)
|
|
800
|
+
writeFileAtomic(THREAD_MODEL_COPIES_CLEARED, "Threads follow their bot's model; the copies older builds made are cleared.\n", { mode: 0o600 });
|
|
776
801
|
// Search reads SQLite directly, so migrate every known legacy transcript
|
|
777
802
|
// at startup rather than waiting until the user happens to open it. Only
|
|
778
803
|
// pending JSON files are touched; already-migrated threads stay lazy.
|
|
@@ -895,10 +920,13 @@ export class Store {
|
|
|
895
920
|
if (botsDirty)
|
|
896
921
|
this.saveBots();
|
|
897
922
|
}
|
|
923
|
+
// Many saves repeat what is already on disk (the same resume cursor, an
|
|
924
|
+
// unchanged task field), so both registries skip a byte-identical write.
|
|
925
|
+
// Events are still emitted by the callers either way.
|
|
898
926
|
saveBots(bots = this.bots, registerSections = true) {
|
|
899
927
|
if (registerSections)
|
|
900
928
|
this.rememberSections([...this.bots, ...bots].map((bot) => bot.section));
|
|
901
|
-
|
|
929
|
+
writeFileAtomicIfChanged(BOTS_FILE, JSON.stringify(bots.map(({ busy: _busy, activity: _activity, ...bot }) => ({
|
|
902
930
|
...bot,
|
|
903
931
|
tasks: bot.tasks?.map(({ busy: _taskBusy, activity: _taskActivity, turnStartedAt: _taskTurnStarted, ...task }) => persistedPin(task)),
|
|
904
932
|
})), null, 2), { mode: 0o600 });
|
|
@@ -906,7 +934,7 @@ export class Store {
|
|
|
906
934
|
saveGroups(groups = this.groups, registerSections = true) {
|
|
907
935
|
if (registerSections)
|
|
908
936
|
this.rememberSections(groups.map((group) => group.section));
|
|
909
|
-
|
|
937
|
+
writeFileAtomicIfChanged(GROUPS_FILE, JSON.stringify(groups.map(({ busyBotId: _busyBotId, turnStartedAt: _turnStartedAt, ...g }) => ({
|
|
910
938
|
...g,
|
|
911
939
|
...(g.tasks ? { tasks: g.tasks.map((task) => persistedPin(task)) } : {}),
|
|
912
940
|
})), null, 2), { mode: 0o600 });
|
|
@@ -1119,6 +1147,9 @@ export class Store {
|
|
|
1119
1147
|
else if (!patch.busyBotId)
|
|
1120
1148
|
delete group.turnStartedAt;
|
|
1121
1149
|
}
|
|
1150
|
+
if (Object.prototype.hasOwnProperty.call(patch, "turnTimeoutMinutes") && patch.turnTimeoutMinutes == null) {
|
|
1151
|
+
delete group.turnTimeoutMinutes;
|
|
1152
|
+
}
|
|
1122
1153
|
if (!group.dm && Object.prototype.hasOwnProperty.call(patch, "pinnedMessageId")) {
|
|
1123
1154
|
const active = this.activeGroupTask(group.id);
|
|
1124
1155
|
if (active)
|
|
@@ -1259,6 +1290,20 @@ export class Store {
|
|
|
1259
1290
|
this.emit({ type: "group", groupId });
|
|
1260
1291
|
return task;
|
|
1261
1292
|
}
|
|
1293
|
+
/** Null removes the conversation's own ceiling so the next turn uses the
|
|
1294
|
+
* global group limit. A sibling conversation is left alone. */
|
|
1295
|
+
setGroupTaskTurnTimeout(groupId, threadId, minutes) {
|
|
1296
|
+
const task = this.groupTaskByThread(groupId, threadId);
|
|
1297
|
+
if (!task)
|
|
1298
|
+
return null;
|
|
1299
|
+
if (minutes == null)
|
|
1300
|
+
delete task.turnTimeoutMinutes;
|
|
1301
|
+
else
|
|
1302
|
+
task.turnTimeoutMinutes = minutes;
|
|
1303
|
+
this.saveGroups();
|
|
1304
|
+
this.emit({ type: "group", groupId });
|
|
1305
|
+
return task;
|
|
1306
|
+
}
|
|
1262
1307
|
renameGroupTask(groupId, threadId, title) {
|
|
1263
1308
|
const task = this.groupTaskByThread(groupId, threadId);
|
|
1264
1309
|
if (!task)
|
|
@@ -1625,7 +1670,6 @@ export class Store {
|
|
|
1625
1670
|
createdAt: bot.createdAt,
|
|
1626
1671
|
updatedAt: bot.createdAt,
|
|
1627
1672
|
resumeCursors: {},
|
|
1628
|
-
modelSelection: structuredClone(bot.modelSelection),
|
|
1629
1673
|
unread: false,
|
|
1630
1674
|
activity: "idle",
|
|
1631
1675
|
busy: false,
|
|
@@ -1657,7 +1701,8 @@ export class Store {
|
|
|
1657
1701
|
return bot;
|
|
1658
1702
|
}
|
|
1659
1703
|
/** All setup fields and the Chief's receipt commit before publishing any
|
|
1660
|
-
* mutation.
|
|
1704
|
+
* mutation. A new model moves the threads that follow the bot, never a
|
|
1705
|
+
* thread's own model. */
|
|
1661
1706
|
applyTeamSetup(request) {
|
|
1662
1707
|
const chief = this.bot(request.botId);
|
|
1663
1708
|
if (!chief)
|
|
@@ -1686,7 +1731,7 @@ export class Store {
|
|
|
1686
1731
|
// a restricted Chief never creates a bot everyone sees.
|
|
1687
1732
|
...(chief.visibility && chief.visibility !== "everyone" ? { visibility: structuredClone(chief.visibility) } : {}),
|
|
1688
1733
|
tasks: [{ threadId: operation.threadId, title: UNTITLED_THREAD, createdAt, updatedAt: createdAt, resumeCursors: {},
|
|
1689
|
-
|
|
1734
|
+
approvalMode: "ask", autoApprove: false,
|
|
1690
1735
|
unread: false, activity: "idle", busy: false }],
|
|
1691
1736
|
};
|
|
1692
1737
|
// "" is the private-workspace spelling on proposal; the record
|
|
@@ -1701,13 +1746,7 @@ export class Store {
|
|
|
1701
1746
|
const previous = nextBots[at];
|
|
1702
1747
|
next = { ...previous, ...operation.fields };
|
|
1703
1748
|
if (operation.fields.modelSelection)
|
|
1704
|
-
next.tasks = previous.
|
|
1705
|
-
...task,
|
|
1706
|
-
modelSelection: structuredClone(task.modelSelection ?? previous.modelSelection),
|
|
1707
|
-
approvalMode: approvalModeFor(this.projectBotForTask(previous.id, task.threadId)),
|
|
1708
|
-
autoApprove: task.autoApprove ?? previous.autoApprove,
|
|
1709
|
-
alwaysAllow: structuredClone(task.alwaysAllow ?? previous.alwaysAllow ?? []),
|
|
1710
|
-
}));
|
|
1749
|
+
next.tasks = this.withDefaultModel(previous, operation.fields.modelSelection);
|
|
1711
1750
|
nextBots[at] = next;
|
|
1712
1751
|
}
|
|
1713
1752
|
if (operation.fields.chiefOfStaff === false)
|
|
@@ -1745,23 +1784,38 @@ export class Store {
|
|
|
1745
1784
|
this.emit({ type: "bot", botId: chief.id });
|
|
1746
1785
|
return result;
|
|
1747
1786
|
}
|
|
1748
|
-
/**
|
|
1749
|
-
*
|
|
1750
|
-
*
|
|
1751
|
-
|
|
1752
|
-
|
|
1787
|
+
/** Moving this thread from one model to another takes its level back to
|
|
1788
|
+
* Ask where the new engine would have to confirm it (modelSwitchNeedsAsk),
|
|
1789
|
+
* as on every engine switch. Read before anything changes. */
|
|
1790
|
+
switchNeedsAsk(botId, threadId, from, to) {
|
|
1791
|
+
if (from.instanceId === to.instanceId)
|
|
1792
|
+
return false;
|
|
1793
|
+
const thread = this.projectBotForTask(botId, threadId);
|
|
1794
|
+
return Boolean(thread) && modelSwitchNeedsAsk(approvalModeFor(thread), this.driverKindOf(from.instanceId), this.driverKindOf(to.instanceId));
|
|
1795
|
+
}
|
|
1796
|
+
/** A bot's threads as a change of its model to `next` leaves them. Each
|
|
1797
|
+
* keeps the level it runs at now (older threads may still inherit the
|
|
1798
|
+
* bot's, which may change in the same write); one that follows the bot
|
|
1799
|
+
* moves with it, back to Ask where the new engine would have to confirm
|
|
1800
|
+
* its level; a thread's own model stays. */
|
|
1801
|
+
withDefaultModel(bot, next) {
|
|
1802
|
+
return bot.tasks?.map((task) => ({
|
|
1803
|
+
...task,
|
|
1804
|
+
approvalMode: approvalModeFor(this.projectBotForTask(bot.id, task.threadId)),
|
|
1805
|
+
autoApprove: task.autoApprove ?? bot.autoApprove,
|
|
1806
|
+
alwaysAllow: structuredClone(task.alwaysAllow ?? bot.alwaysAllow ?? []),
|
|
1807
|
+
...(task.modelSelection === undefined && this.switchNeedsAsk(bot.id, task.threadId, bot.modelSelection, next) ? structuredClone(ASK_NOW) : {}),
|
|
1808
|
+
}));
|
|
1809
|
+
}
|
|
1810
|
+
/** One reviewed default-model change (propose_model), with
|
|
1811
|
+
* applyTeamSetup's task stamping (withDefaultModel): every thread that
|
|
1812
|
+
* follows the bot moves to the new model, and a thread's own model, which a
|
|
1813
|
+
* person picked there, stays. */
|
|
1753
1814
|
applyModelDefault(id, modelSelection) {
|
|
1754
1815
|
const previous = this.bot(id);
|
|
1755
1816
|
if (!previous)
|
|
1756
1817
|
return null;
|
|
1757
|
-
const next = { ...previous, modelSelection: structuredClone(modelSelection) };
|
|
1758
|
-
next.tasks = previous.tasks?.map((task) => ({
|
|
1759
|
-
...task,
|
|
1760
|
-
modelSelection: structuredClone(task.modelSelection ?? previous.modelSelection),
|
|
1761
|
-
approvalMode: approvalModeFor(this.projectBotForTask(previous.id, task.threadId)),
|
|
1762
|
-
autoApprove: task.autoApprove ?? previous.autoApprove,
|
|
1763
|
-
alwaysAllow: structuredClone(task.alwaysAllow ?? previous.alwaysAllow ?? []),
|
|
1764
|
-
}));
|
|
1818
|
+
const next = { ...previous, modelSelection: structuredClone(modelSelection), tasks: this.withDefaultModel(previous, modelSelection) };
|
|
1765
1819
|
this.saveBots(this.bots.map((candidate) => candidate.id === id ? next : candidate));
|
|
1766
1820
|
this.bots = this.bots.map((candidate) => candidate.id === id ? next : candidate);
|
|
1767
1821
|
this.emit({ type: "bot", botId: id });
|
|
@@ -1859,9 +1913,16 @@ export class Store {
|
|
|
1859
1913
|
patch = { ...patch };
|
|
1860
1914
|
delete patch.toolScope;
|
|
1861
1915
|
}
|
|
1916
|
+
// A new model moves the threads that follow the bot; one whose level the
|
|
1917
|
+
// new engine would have to confirm goes back to Ask. That is a
|
|
1918
|
+
// revocation, so it holds in memory like the others below.
|
|
1919
|
+
const toAsk = patch.modelSelection ? (bot.tasks ?? []).filter((task) => task.modelSelection === undefined &&
|
|
1920
|
+
this.switchNeedsAsk(id, task.threadId, bot.modelSelection, patch.modelSelection)) : [];
|
|
1862
1921
|
// Runtime revocations must become effective in memory even when disk is
|
|
1863
1922
|
// unavailable. Profile edits use the separate atomic path below.
|
|
1864
1923
|
Object.assign(bot, patch);
|
|
1924
|
+
for (const task of toAsk)
|
|
1925
|
+
Object.assign(task, structuredClone(ASK_NOW));
|
|
1865
1926
|
const task = this.activeTask(id);
|
|
1866
1927
|
if (task) {
|
|
1867
1928
|
for (const key of ["resumeCursors", "rewound", "pinnedMessageId", "unread"]) {
|
|
@@ -2144,6 +2205,129 @@ export class Store {
|
|
|
2144
2205
|
this.emit({ type: "bot", botId: bot.id });
|
|
2145
2206
|
return changed.length;
|
|
2146
2207
|
}
|
|
2208
|
+
/** The removed Computer engine's saved choices move to `replacement` in one
|
|
2209
|
+
* save: each bot's model, each conversation's own model, and its backups.
|
|
2210
|
+
* Native resume cursors and handed-message records of the removed engine go
|
|
2211
|
+
* too; the conversations stay, and the new engine reads them from the
|
|
2212
|
+
* transcript. Each conversation keeps working where it did: on Auto the
|
|
2213
|
+
* removed engine always ran on the bot's own cloud computer, so where
|
|
2214
|
+
* `keepCloud` says that computer is still there and the new engine can use
|
|
2215
|
+
* it, a moved bot's Works on becomes Cloud, and a moved conversation of an
|
|
2216
|
+
* Auto bot is pinned there the way an Auto turn records where it landed.
|
|
2217
|
+
* Where the new engine can't use a computer, an Auto-recorded cloud pin on
|
|
2218
|
+
* a moved conversation gives way, as on a Works on change
|
|
2219
|
+
* (clearAutoSurfacePins); a place the person chose stays chosen.
|
|
2220
|
+
* A level the new engine would have to confirm goes back to Ask, as on
|
|
2221
|
+
* every engine switch (modelSwitchNeedsAsk); a conversation that followed
|
|
2222
|
+
* the bot's level keeps the level it had. Every other setting is kept.
|
|
2223
|
+
* Nothing changes in memory until the save succeeds, so a failed save can
|
|
2224
|
+
* be tried again. Returns one entry per moved conversation, plus one for
|
|
2225
|
+
* the bot's open conversation when only the bot's own engine moved.
|
|
2226
|
+
* Idempotent: nothing left names the engine. */
|
|
2227
|
+
retireInstances(ids, replacement, options) {
|
|
2228
|
+
const retired = (selection) => Boolean(selection && ids.has(selection.instanceId));
|
|
2229
|
+
const without = (record) => Object.fromEntries(Object.entries(record).filter(([key]) => !ids.has(key)));
|
|
2230
|
+
const touches = (record) => Boolean(record && Object.keys(record).some(key => ids.has(key)));
|
|
2231
|
+
const needsAsk = (target) => modelSwitchNeedsAsk(approvalModeFor(target), REMOVED_COMPUTER_DRIVER, options.driverKind);
|
|
2232
|
+
const askNow = { approvalMode: "ask", autoApprove: false, alwaysAllow: [] };
|
|
2233
|
+
const changes = [];
|
|
2234
|
+
const moves = [];
|
|
2235
|
+
for (const bot of this.bots) {
|
|
2236
|
+
const patch = {};
|
|
2237
|
+
const tasks = (bot.tasks ?? []).map(() => ({}));
|
|
2238
|
+
const botMoved = retired(bot.modelSelection);
|
|
2239
|
+
const keepsCloud = options.canWorkOnCloud && options.keepCloud(bot);
|
|
2240
|
+
let botAsk = false;
|
|
2241
|
+
if (botMoved) {
|
|
2242
|
+
if (bot.computer === undefined && keepsCloud)
|
|
2243
|
+
patch.computer = "cloud";
|
|
2244
|
+
if (needsAsk(bot)) {
|
|
2245
|
+
Object.assign(patch, structuredClone(askNow));
|
|
2246
|
+
botAsk = true;
|
|
2247
|
+
}
|
|
2248
|
+
patch.modelSelection = structuredClone(replacement);
|
|
2249
|
+
}
|
|
2250
|
+
if (bot.fallback?.some(retired)) {
|
|
2251
|
+
const kept = bot.fallback.filter(candidate => !retired(candidate));
|
|
2252
|
+
patch.fallback = kept.length ? kept : undefined;
|
|
2253
|
+
}
|
|
2254
|
+
if (touches(bot.resumeCursors))
|
|
2255
|
+
patch.resumeCursors = without(bot.resumeCursors);
|
|
2256
|
+
const computer = patch.computer ?? bot.computer;
|
|
2257
|
+
/** Where a conversation worked before the move (its own place, else
|
|
2258
|
+
* the bot's Works on, and on Auto the bot's own cloud computer where
|
|
2259
|
+
* keepCloud says so), and what the move did to that. */
|
|
2260
|
+
const cloudAfterMove = (pin, pinSource) => {
|
|
2261
|
+
const place = pin ?? bot.computer;
|
|
2262
|
+
const wasCloud = place === "cloud" || (place === undefined && options.keepCloud(bot));
|
|
2263
|
+
if (!wasCloud || options.canWorkOnCloud)
|
|
2264
|
+
return { cloud: wasCloud, noComputer: false };
|
|
2265
|
+
// An Auto-recorded pin gives way (below): only the person's place
|
|
2266
|
+
// or the bot's Works on can still send it to the cloud computer.
|
|
2267
|
+
const personPin = pin !== undefined && pinSource !== "auto";
|
|
2268
|
+
const still = personPin ? pin : bot.computer;
|
|
2269
|
+
return { cloud: false, noComputer: still !== "cloud" ? "auto" : personPin ? "pin" : "works-on" };
|
|
2270
|
+
};
|
|
2271
|
+
(bot.tasks ?? []).forEach((task, index) => {
|
|
2272
|
+
const taskPatch = tasks[index];
|
|
2273
|
+
// Read before anything changes: a conversation that follows the bot's
|
|
2274
|
+
// level has the bot's level here.
|
|
2275
|
+
const before = this.projectBotForTask(bot.id, task.threadId);
|
|
2276
|
+
// The bot's own level goes back to Ask, so a conversation that
|
|
2277
|
+
// followed it keeps the level it had (as switchTaskModel does).
|
|
2278
|
+
if (botAsk) {
|
|
2279
|
+
Object.assign(taskPatch, {
|
|
2280
|
+
approvalMode: before.approvalMode, autoApprove: before.autoApprove, alwaysAllow: structuredClone(before.alwaysAllow ?? []),
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
// The engine this conversation ran on: its own, else the bot's.
|
|
2284
|
+
const moved = task.modelSelection ? retired(task.modelSelection) : botMoved;
|
|
2285
|
+
if (moved) {
|
|
2286
|
+
if (!botMoved && computer === undefined && task.surface === undefined && keepsCloud) {
|
|
2287
|
+
taskPatch.surface = "cloud";
|
|
2288
|
+
taskPatch.surfaceSource = "auto";
|
|
2289
|
+
}
|
|
2290
|
+
// The machine's memory of where Auto landed: with no computer on
|
|
2291
|
+
// the new engine, the next turn would be refused there once.
|
|
2292
|
+
if (!options.canWorkOnCloud && task.surface === "cloud" && task.surfaceSource === "auto") {
|
|
2293
|
+
taskPatch.surface = undefined;
|
|
2294
|
+
taskPatch.surfaceSource = undefined;
|
|
2295
|
+
}
|
|
2296
|
+
const ask = needsAsk(before);
|
|
2297
|
+
if (ask)
|
|
2298
|
+
Object.assign(taskPatch, structuredClone(askNow));
|
|
2299
|
+
if (task.modelSelection)
|
|
2300
|
+
taskPatch.modelSelection = structuredClone(replacement);
|
|
2301
|
+
const scope = botMoved && task.threadId === bot.threadId ? "bot" : "conversation";
|
|
2302
|
+
moves.push({ botId: bot.id, threadId: task.threadId, scope, ...cloudAfterMove(task.surface, task.surfaceSource),
|
|
2303
|
+
askNow: ask || (scope === "bot" && botAsk) });
|
|
2304
|
+
}
|
|
2305
|
+
else if (botMoved && task.threadId === bot.threadId) {
|
|
2306
|
+
// The bot's own engine and level moved, but its open conversation
|
|
2307
|
+
// has an engine of its own: it is still told, about the bot.
|
|
2308
|
+
moves.push({ botId: bot.id, threadId: task.threadId, scope: "bot-only", ...cloudAfterMove(undefined, undefined), askNow: botAsk });
|
|
2309
|
+
}
|
|
2310
|
+
if (touches(task.resumeCursors))
|
|
2311
|
+
taskPatch.resumeCursors = without(task.resumeCursors);
|
|
2312
|
+
if (task.handedMessages && touches(task.handedMessages))
|
|
2313
|
+
taskPatch.handedMessages = without(task.handedMessages);
|
|
2314
|
+
});
|
|
2315
|
+
if (Object.keys(patch).length || tasks.some(taskPatch => Object.keys(taskPatch).length))
|
|
2316
|
+
changes.push({ bot, patch, tasks });
|
|
2317
|
+
}
|
|
2318
|
+
if (!changes.length)
|
|
2319
|
+
return moves;
|
|
2320
|
+
const next = new Map(changes.map(({ bot, patch, tasks }) => [bot, {
|
|
2321
|
+
...bot, ...patch, tasks: bot.tasks?.map((task, index) => ({ ...task, ...tasks[index] })),
|
|
2322
|
+
}]));
|
|
2323
|
+
this.saveBots(this.bots.map(bot => next.get(bot) ?? bot));
|
|
2324
|
+
for (const { bot, patch, tasks } of changes) {
|
|
2325
|
+
bot.tasks?.forEach((task, index) => Object.assign(task, tasks[index]));
|
|
2326
|
+
Object.assign(bot, patch);
|
|
2327
|
+
this.emit({ type: "bot", botId: bot.id });
|
|
2328
|
+
}
|
|
2329
|
+
return moves;
|
|
2330
|
+
}
|
|
2147
2331
|
setResumeCursor(botId, instanceId, cursor, threadId) {
|
|
2148
2332
|
const bot = this.bot(botId);
|
|
2149
2333
|
if (!bot)
|
|
@@ -2462,37 +2646,61 @@ export class Store {
|
|
|
2462
2646
|
this.emit({ type: "bot", botId });
|
|
2463
2647
|
return cleared;
|
|
2464
2648
|
}
|
|
2465
|
-
/**
|
|
2466
|
-
*
|
|
2467
|
-
*
|
|
2649
|
+
/** A person picks a model in one thread, and with updateBotDefault makes it
|
|
2650
|
+
* the bot's model too. A thread on its bot's model follows the bot: it
|
|
2651
|
+
* keeps no model of its own. Model/provider changes are one configuration
|
|
2652
|
+
* transaction: never publish a new provider before its confirmed approval
|
|
2653
|
+
* downgrade, or change the default while leaving the selected thread
|
|
2654
|
+
* behind after a write failure. */
|
|
2468
2655
|
switchTaskModel(botId, threadId, selection, updateBotDefault, resetApprovalToAsk, taskPatch = {}) {
|
|
2469
2656
|
const bot = this.bot(botId);
|
|
2470
2657
|
const task = this.taskByThread(botId, threadId);
|
|
2471
2658
|
if (!bot || !task)
|
|
2472
2659
|
return null;
|
|
2473
|
-
const
|
|
2474
|
-
|
|
2475
|
-
const nextTask = persistedPin({ ...task, ...taskPatch, ...
|
|
2660
|
+
const approval = resetApprovalToAsk ? structuredClone(ASK_NOW) : {};
|
|
2661
|
+
const own = sameModelSelection(selection, updateBotDefault ? selection : bot.modelSelection) ? undefined : structuredClone(selection);
|
|
2662
|
+
const nextTask = persistedPin({ ...task, ...taskPatch, modelSelection: own, ...approval,
|
|
2476
2663
|
...(typeof taskPatch.title === "string" ? { title: taskPatch.title.trim().slice(0, 80) || UNTITLED_THREAD } : {}) });
|
|
2477
|
-
//
|
|
2478
|
-
//
|
|
2479
|
-
|
|
2480
|
-
const nextTasks = bot.tasks.map((candidate) => candidate === task ? nextTask :
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
approvalMode: approvalModeFor(this.projectBotForTask(botId, candidate.threadId)),
|
|
2484
|
-
autoApprove: candidate.autoApprove ?? bot.autoApprove,
|
|
2485
|
-
alwaysAllow: structuredClone(candidate.alwaysAllow ?? bot.alwaysAllow ?? []),
|
|
2486
|
-
});
|
|
2487
|
-
const next = { ...bot, ...(updateBotDefault ? patch : {}),
|
|
2488
|
-
tasks: nextTasks };
|
|
2489
|
-
this.saveBots(this.bots.map((candidate) => candidate === bot ? next : candidate));
|
|
2664
|
+
// A new bot model moves the threads that follow it (withDefaultModel);
|
|
2665
|
+
// every other thread keeps its model and the level it runs at now.
|
|
2666
|
+
const others = updateBotDefault ? this.withDefaultModel(bot, selection) : bot.tasks;
|
|
2667
|
+
const nextTasks = bot.tasks.map((candidate, index) => candidate === task ? nextTask : others[index]);
|
|
2668
|
+
const botPatch = updateBotDefault ? { modelSelection: structuredClone(selection), ...approval } : {};
|
|
2669
|
+
this.saveBots(this.bots.map((candidate) => candidate === bot ? { ...bot, ...botPatch, tasks: nextTasks } : candidate));
|
|
2490
2670
|
bot.tasks.forEach((candidate, index) => Object.assign(candidate, nextTasks[index]));
|
|
2491
|
-
|
|
2492
|
-
Object.assign(bot, patch);
|
|
2671
|
+
Object.assign(bot, botPatch);
|
|
2493
2672
|
this.emit({ type: "bot", botId });
|
|
2494
2673
|
return task;
|
|
2495
2674
|
}
|
|
2675
|
+
/** These threads follow their bot's model again: each one's own model is
|
|
2676
|
+
* cleared ("Switch them too", "Use the bot's model", or a model that can't
|
|
2677
|
+
* run here). A level the bot's engine would have to confirm goes back to
|
|
2678
|
+
* Ask, as on every engine switch, and `reset` adds what a changed model
|
|
2679
|
+
* needs (a hosted workspace's fresh session). One write. Returns the
|
|
2680
|
+
* threads that changed; a listed thread that already follows stays as it is. */
|
|
2681
|
+
followBotModel(botId, threadIds, reset = () => ({})) {
|
|
2682
|
+
const bot = this.bot(botId);
|
|
2683
|
+
if (!bot?.tasks)
|
|
2684
|
+
return [];
|
|
2685
|
+
const listed = new Set(threadIds);
|
|
2686
|
+
const patches = bot.tasks.map((task) => !listed.has(task.threadId) || task.modelSelection === undefined ? null : {
|
|
2687
|
+
...reset(task.modelSelection, bot.modelSelection),
|
|
2688
|
+
modelSelection: undefined,
|
|
2689
|
+
...(this.switchNeedsAsk(botId, task.threadId, task.modelSelection, bot.modelSelection) ? structuredClone(ASK_NOW) : {}),
|
|
2690
|
+
});
|
|
2691
|
+
const moved = bot.tasks.filter((_, index) => patches[index]).map((task) => task.threadId);
|
|
2692
|
+
if (!moved.length)
|
|
2693
|
+
return [];
|
|
2694
|
+
const nextTasks = bot.tasks.map((task, index) => patches[index] ? { ...task, ...patches[index] } : task);
|
|
2695
|
+
this.saveBots(this.bots.map((candidate) => candidate === bot ? { ...bot, tasks: nextTasks } : candidate));
|
|
2696
|
+
bot.tasks.forEach((task, index) => { if (patches[index])
|
|
2697
|
+
Object.assign(task, patches[index]); });
|
|
2698
|
+
const active = bot.tasks.find((task) => task.threadId === bot.threadId);
|
|
2699
|
+
if (active)
|
|
2700
|
+
this.mirrorActiveTask(bot, active);
|
|
2701
|
+
this.emit({ type: "bot", botId });
|
|
2702
|
+
return moved;
|
|
2703
|
+
}
|
|
2496
2704
|
/** One durable write: never leave only part of a bot's threads updated. */
|
|
2497
2705
|
setAllThreadApprovalMode(botId, mode) {
|
|
2498
2706
|
const bot = this.bot(botId);
|
|
@@ -2532,9 +2740,9 @@ export class Store {
|
|
|
2532
2740
|
bot.pinnedMessageId = task.pinnedMessageId;
|
|
2533
2741
|
}
|
|
2534
2742
|
/** A fresh context on the same bot: new thread, new session, same
|
|
2535
|
-
* persona/tools/computer. Becomes the active task. It
|
|
2536
|
-
*
|
|
2537
|
-
* another of this bot's threads keeps
|
|
2743
|
+
* persona/tools/computer. Becomes the active task. It follows the bot's
|
|
2744
|
+
* model unless the caller hands it another one: a thread opened from
|
|
2745
|
+
* another of this bot's threads keeps the model a person picked there. */
|
|
2538
2746
|
createTask(botId, title, activate = true, projectId, openedBy, approvalMode, modelSelection) {
|
|
2539
2747
|
const bot = this.bot(botId);
|
|
2540
2748
|
if (!bot)
|
|
@@ -2550,7 +2758,7 @@ export class Store {
|
|
|
2550
2758
|
...(projectId ? { projectId } : {}),
|
|
2551
2759
|
...(openedBy ? { openedBy: structuredClone(openedBy) } : {}),
|
|
2552
2760
|
resumeCursors: {},
|
|
2553
|
-
modelSelection
|
|
2761
|
+
...(modelSelection && !sameModelSelection(modelSelection, bot.modelSelection) ? { modelSelection: structuredClone(modelSelection) } : {}),
|
|
2554
2762
|
approvalMode: approvalMode ?? approvalModeFor(bot),
|
|
2555
2763
|
autoApprove: approvalMode ? false : Boolean(bot.autoApprove),
|
|
2556
2764
|
alwaysAllow: approvalMode ? [] : [...(bot.alwaysAllow ?? [])],
|
|
@@ -2656,10 +2864,14 @@ export class Store {
|
|
|
2656
2864
|
this.emit({ type: "bot", botId });
|
|
2657
2865
|
return bot;
|
|
2658
2866
|
}
|
|
2867
|
+
/** A first run: no bot yet, so seedIfEmpty makes one. */
|
|
2868
|
+
needsSeed() {
|
|
2869
|
+
return this.bots.length === 0;
|
|
2870
|
+
}
|
|
2659
2871
|
/** First-run seed: one bot so the app never opens empty — it gets a
|
|
2660
2872
|
* random friendly name like every other bot. */
|
|
2661
2873
|
seedIfEmpty() {
|
|
2662
|
-
if (this.
|
|
2874
|
+
if (!this.needsSeed())
|
|
2663
2875
|
return;
|
|
2664
2876
|
this.createBot();
|
|
2665
2877
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// decides or describes a surface lives here, so the picker, the dispatch,
|
|
7
7
|
// the system prompt and the notification cannot drift apart.
|
|
8
8
|
import { canWorkOnCloud } from "../shared/cloud-computer.js";
|
|
9
|
+
import { canUseMcpServer } from "../shared/tool-scope.js";
|
|
10
|
+
import { placeRowText } from "../shared/place-view.js";
|
|
9
11
|
const SURFACES = new Set(["cloud", "vm", "local", "browser"]);
|
|
10
12
|
/** Parse a surface arriving over the wire; anything else is "not said". */
|
|
11
13
|
export function parseSurface(value) {
|
|
@@ -68,65 +70,43 @@ export function resolveSurface(input) {
|
|
|
68
70
|
}
|
|
69
71
|
return { computer: undefined, browser: browserOn, pinned: null, note: "" };
|
|
70
72
|
}
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
},
|
|
78
|
-
// The dispatch clears an Auto-recorded pin that failed, so the next
|
|
79
|
-
// message runs on Auto: that pin was the machine's memory, not a choice.
|
|
80
|
-
"auto-pin": {
|
|
81
|
-
sentence: "This conversation is back on Auto; send your message again.",
|
|
82
|
-
clause: "send your message again; this conversation is back on Auto",
|
|
83
|
-
},
|
|
84
|
-
"works-on": {
|
|
85
|
-
sentence: "Set Works on to Auto in this bot's settings to continue.",
|
|
86
|
-
clause: "set Works on to Auto",
|
|
87
|
-
},
|
|
88
|
-
routine: {
|
|
89
|
-
sentence: "Change where this routine runs.",
|
|
90
|
-
clause: "change where this routine runs",
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
/** Why this engine can't work on the cloud computer, in the words of the
|
|
94
|
-
* place's source, or null when it can (shared/cloud-computer.ts holds the
|
|
95
|
-
* rule). Checked before anything is provisioned, so a turn that cannot run
|
|
96
|
-
* never creates or wakes a machine. */
|
|
97
|
-
export function cloudPlaceDriverError(engine, backend, source = "works-on") {
|
|
98
|
-
if (canWorkOnCloud(engine, backend))
|
|
73
|
+
/** Why this engine can't work on the cloud computer, as a failed place, or
|
|
74
|
+
* null when it can (shared/cloud-computer.ts holds the rule). Checked before
|
|
75
|
+
* anything is provisioned, so a turn that cannot run never creates or wakes
|
|
76
|
+
* a machine. */
|
|
77
|
+
export function cloudPlaceRefusal(engine, source, bot) {
|
|
78
|
+
if (canWorkOnCloud(engine))
|
|
99
79
|
return null;
|
|
100
|
-
|
|
101
|
-
return engine.driverKind === "boxAgent"
|
|
102
|
-
? `The Computer engine runs on Boat and can't use a self-hosted VPS. ${next}`
|
|
103
|
-
: `This model can't use a computer. ${next}`;
|
|
80
|
+
return placeUnavailable("cloud", { state: "cc-cannot", params: { bot, model: engine.name }, source });
|
|
104
81
|
}
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
return
|
|
114
|
-
return
|
|
82
|
+
/** Why this bot's Tool selection keeps it off a computer, as a failed place,
|
|
83
|
+
* or null: one line and the one setting that changes it, the same whatever
|
|
84
|
+
* chose the place (a turn's refusal and select_computer's reason alike).
|
|
85
|
+
* Engines reach every desktop through the "computer" MCP server, so a
|
|
86
|
+
* selection without it has nothing to work with there. Checked with the
|
|
87
|
+
* engine rule, before anything is created or woken. */
|
|
88
|
+
export function computerToolsRefusal(toolScope, source, bot) {
|
|
89
|
+
if (canUseMcpServer(toolScope, "computer"))
|
|
90
|
+
return null;
|
|
91
|
+
return placeUnavailable("cloud", { state: "cc-tools-off", params: { bot }, source });
|
|
115
92
|
}
|
|
116
|
-
/** A place the turn was told to use could not be used.
|
|
117
|
-
*
|
|
93
|
+
/** A place the turn was told to use could not be used. Its message is the
|
|
94
|
+
* row's English words: one line and the one next action as a sentence
|
|
95
|
+
* (shared/place-view.ts), uncut. `row` lets the app word it again in the
|
|
96
|
+
* reader's language, and `place` lets the dispatch clear a failed
|
|
118
97
|
* Auto-recorded pin to it, so a failed place never sticks. */
|
|
119
98
|
export class PlaceUnavailableError extends Error {
|
|
120
99
|
place;
|
|
121
|
-
|
|
122
|
-
|
|
100
|
+
row;
|
|
101
|
+
constructor(place, row, cloudHome = false) {
|
|
102
|
+
super(placeRowText(row, cloudHome ? "my-cloud" : undefined));
|
|
123
103
|
this.name = "PlaceUnavailableError";
|
|
124
104
|
this.place = place;
|
|
105
|
+
this.row = row;
|
|
125
106
|
}
|
|
126
107
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return new PlaceUnavailableError(place, placeFailureMessage(cause, source, limit));
|
|
108
|
+
export function placeUnavailable(place, row, cloudHome = false) {
|
|
109
|
+
return new PlaceUnavailableError(place, row, cloudHome);
|
|
130
110
|
}
|
|
131
111
|
/** How the prompt and the app name a surface. Deliberately the same words
|
|
132
112
|
* the panel uses, so "tell them it is on the cloud computer" points at a
|