openmausbot 0.1.92 → 0.1.94
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-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
- package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
- package/dist/assets/channel-CdJX6i8k.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
- package/dist/assets/index-CPYrGsv1.js +610 -0
- package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
- package/dist/assets/index-CzT42GR8.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
- package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
- package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -0
- package/dist-server/companion/src/routes.js +45 -1
- package/dist-server/container-mcp.js +99 -77
- package/dist-server/drivers/agents-proxy.js +40 -1
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +1 -0
- package/dist-server/index.js +18361 -13794
- package/dist-server/local-computer-proxy.js +5 -1
- package/dist-server/local-computer.js +1 -0
- package/dist-server/mcp-gate.js +152 -20
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +8872 -7620
- package/dist-server/pair-cli.js +8872 -7620
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/auto-approve.js +33 -11
- package/dist-server/server/boat.js +3 -3
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +6 -0
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +57 -10
- package/dist-server/server/container-computer.js +43 -32
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +229 -20
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-call.js +28 -0
- package/dist-server/server/drivers/agents-catalog.js +10 -1
- package/dist-server/server/drivers/boatagent.js +2 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
- package/dist-server/server/drivers/claude.js +35 -9
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +37 -5
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/env-path.js +6 -2
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/index.js +1353 -311
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -0
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/mcp-gate-config.js +43 -12
- package/dist-server/server/mcp-gate.js +127 -25
- package/dist-server/server/mcp-http.js +46 -8
- package/dist-server/server/mcp-remote-proxy.js +108 -0
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/peer-provenance.js +4 -2
- package/dist-server/server/permission-proxy.js +15 -1
- package/dist-server/server/plan-usage.js +797 -0
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/routes/desktop-viewer.js +50 -6
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/skill-library.js +207 -0
- package/dist-server/server/skills-library-migration.js +235 -0
- package/dist-server/server/skills.js +66 -3
- package/dist-server/server/steer-queue.js +13 -1
- package/dist-server/server/store.js +86 -4
- package/dist-server/server/surface.js +9 -3
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/thread-events.js +1 -1
- package/dist-server/server/tunnel.js +20 -1
- package/dist-server/server/turn-log.js +10 -0
- package/dist-server/server/turn-resources.js +124 -0
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup.js +22 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +13 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/tool-scope-support.js +25 -0
- package/dist-server/shared/tool-scope.js +136 -0
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +99 -77
- package/dist-server/workspace-backup.worker.js +103 -100
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +0 -1
- package/dist/assets/index-Cdmjrl28.css +0 -1
- package/dist/assets/index-DR7YPMkW.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
|
@@ -14,7 +14,6 @@ import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-ico
|
|
|
14
14
|
import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
|
|
15
15
|
import { parseJson, schemaIssue } from "./schema.js";
|
|
16
16
|
import { CLOUD_SEAT_IDLE_STOP_MS } from "./cloud-overflow.js";
|
|
17
|
-
import { cloudHomeConfigured } from "./cloud-home.js";
|
|
18
17
|
const optionalText = z.string().optional();
|
|
19
18
|
const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
20
19
|
const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
@@ -304,6 +303,10 @@ const featureConfigSchema = z.object({
|
|
|
304
303
|
* consent card or a configured allowlist — never a silent Auto default.
|
|
305
304
|
* Off until baked; see cloudOverflowEnabled for how to enable it by hand. */
|
|
306
305
|
cloudOverflow: z.boolean().optional(),
|
|
306
|
+
/** Opt-in shared skills library: one store at the data dir that bots
|
|
307
|
+
* read by assignment instead of per-workspace copies. Off until
|
|
308
|
+
* explicitly enabled — see skillsLibraryEnabled. */
|
|
309
|
+
skillsLibrary: z.boolean().optional(),
|
|
307
310
|
});
|
|
308
311
|
/** First-run progress. Kept in the workspace config rather than a browser so
|
|
309
312
|
* it survives cleared site data and is shared by every paired client. Hint
|
|
@@ -388,6 +391,7 @@ const appConfigSchema = z.object({
|
|
|
388
391
|
phone: z.enum(["ios", "android"]).optional(),
|
|
389
392
|
}).optional(),
|
|
390
393
|
mistral: z.object({ key: optionalText }).optional(),
|
|
394
|
+
cerebras: z.object({ key: optionalText }).optional(),
|
|
391
395
|
xai: z.object({ key: optionalText, url: optionalText }).optional(),
|
|
392
396
|
/** Anthropic API key for Claude Code billed per token, handed only to
|
|
393
397
|
* Claude instances; `url` only for a proxy or a test double. Never a
|
|
@@ -473,6 +477,15 @@ const appConfigSchema = z.object({
|
|
|
473
477
|
.optional(),
|
|
474
478
|
jobs: z.object({ roomRouting: z.boolean().optional() }).optional(),
|
|
475
479
|
}).optional(),
|
|
480
|
+
/** Live calls: an OpenAI project key for GPT-Live, kept apart from every
|
|
481
|
+
* other OpenAI credential so a Live call never bills an image or engine key
|
|
482
|
+
* the user did not hand to it. `voice` is a GPT-Live built-in voice name. */
|
|
483
|
+
live: z.object({
|
|
484
|
+
key: optionalText,
|
|
485
|
+
voice: z.string().trim().max(40).regex(/^[a-z]*$/, "a Live voice is a lowercase built-in voice name").optional(),
|
|
486
|
+
readTypedReplies: z.boolean().optional(),
|
|
487
|
+
idleMinutes: z.number().int().min(1).max(60).optional(),
|
|
488
|
+
}).optional(),
|
|
476
489
|
/** Avatar provider credentials stay separate; choosing a router never reuses a cloud key. */
|
|
477
490
|
imageGen: z.object({
|
|
478
491
|
provider: z.enum(["openai", "xai", "custom"]).optional(),
|
|
@@ -629,6 +642,17 @@ export function browserEngineAttachCdpUrl(cfg) {
|
|
|
629
642
|
export function roomTurnTimeoutMinutes(cfg) {
|
|
630
643
|
return cfg.rooms?.turnTimeoutMinutes ?? DEFAULT_ROOM_TURN_TIMEOUT_MINUTES;
|
|
631
644
|
}
|
|
645
|
+
export const LIVE_IDLE_MINUTES_DEFAULT = 5;
|
|
646
|
+
/** Non-secret Live settings. The key only shows up as `configured`. */
|
|
647
|
+
export function liveSettingsFor(cfg) {
|
|
648
|
+
const minutes = cfg.live?.idleMinutes;
|
|
649
|
+
return {
|
|
650
|
+
configured: Boolean(cfg.live?.key?.trim()),
|
|
651
|
+
voice: cfg.live?.voice ?? "",
|
|
652
|
+
readTypedReplies: cfg.live?.readTypedReplies ?? true,
|
|
653
|
+
idleMinutes: Number.isInteger(minutes) && minutes >= 1 && minutes <= 60 ? minutes : LIVE_IDLE_MINUTES_DEFAULT,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
632
656
|
/** Room handoff tree budgets in milliseconds. The tree lifetime pauses
|
|
633
657
|
* while a node is actively executing; the hard cap is the maximum stall
|
|
634
658
|
* window, measured wall-clock from the tree's last durable progress. Read
|
|
@@ -691,14 +715,11 @@ export function showToolCallsEnabled(cfg) {
|
|
|
691
715
|
export function routinesInConversationEnabled(cfg) {
|
|
692
716
|
return cfg.features?.routinesInConversation === true;
|
|
693
717
|
}
|
|
694
|
-
/** Workspace-level gate for the
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
export function builtInBrowserEnabled(cfg, env = process.env) {
|
|
700
|
-
const chosen = cfg.features?.browser;
|
|
701
|
-
return chosen === undefined ? cloudHomeConfigured(env) : chosen === true;
|
|
718
|
+
/** Workspace-level gate for the built-in browser. A bot's own switch sits
|
|
719
|
+
* under it, so either can withhold the browser. On unless the person switched
|
|
720
|
+
* it off: an explicit `false` (Settings, or a bot's computer panel) is kept. */
|
|
721
|
+
export function builtInBrowserEnabled(cfg, _env = process.env) {
|
|
722
|
+
return cfg.features?.browser !== false;
|
|
702
723
|
}
|
|
703
724
|
/** Opt-in computer sharing: the routes, the agent tools, the advertised
|
|
704
725
|
* capability and the desktop connector. Off unless an explicit `true` turns
|
|
@@ -765,6 +786,14 @@ export function cloudOverflowIdleStopMs(cfg) {
|
|
|
765
786
|
export function cloudOverflowAllowlistedThreads(cfg) {
|
|
766
787
|
return new Set(cfg.cloudOverflow?.allowlistedThreads ?? []);
|
|
767
788
|
}
|
|
789
|
+
/** Opt-in shared skills library (skills lane S1): one store at the data dir
|
|
790
|
+
* that bots reference by assignment instead of per-workspace copies. Off
|
|
791
|
+
* unless enabled by hand in ~/.openmausbot/config.json
|
|
792
|
+
* (`{"features": {"skillsLibrary": true}}`); while off, every skills
|
|
793
|
+
* surface keeps today's byte-identical per-bot behavior. */
|
|
794
|
+
export function skillsLibraryEnabled(cfg) {
|
|
795
|
+
return cfg.features?.skillsLibrary === true;
|
|
796
|
+
}
|
|
768
797
|
/** Config sections no provider driver reads. A write that touches only
|
|
769
798
|
* these must not rebuild the fleet: rebuilding disposes every engine child
|
|
770
799
|
* and reloads it, seconds of work that would also interrupt in-flight
|
|
@@ -777,6 +806,7 @@ export const FLEET_NEUTRAL_KEYS = new Set([
|
|
|
777
806
|
// no engine reads it: the harness asks it before a turn starts
|
|
778
807
|
"decider",
|
|
779
808
|
"imageGen",
|
|
809
|
+
"live",
|
|
780
810
|
"vps",
|
|
781
811
|
"rooms",
|
|
782
812
|
"threads",
|
|
@@ -884,6 +914,9 @@ export function loadConfig() {
|
|
|
884
914
|
cfg.mistral = { ...cfg.mistral };
|
|
885
915
|
if (process.env.MISTRAL_API_KEY !== undefined)
|
|
886
916
|
cfg.mistral.key = process.env.MISTRAL_API_KEY;
|
|
917
|
+
cfg.cerebras = { ...cfg.cerebras };
|
|
918
|
+
if (process.env.CEREBRAS_API_KEY !== undefined)
|
|
919
|
+
cfg.cerebras.key = process.env.CEREBRAS_API_KEY;
|
|
887
920
|
cfg.xai = { ...cfg.xai };
|
|
888
921
|
if (process.env.XAI_API_KEY !== undefined)
|
|
889
922
|
cfg.xai.key = process.env.XAI_API_KEY;
|
|
@@ -933,6 +966,9 @@ export function loadConfig() {
|
|
|
933
966
|
cfg.decider = { ...cfg.decider };
|
|
934
967
|
if (process.env.OMB_JEV_API_KEY !== undefined)
|
|
935
968
|
cfg.decider.key = process.env.OMB_JEV_API_KEY;
|
|
969
|
+
cfg.live = { ...cfg.live };
|
|
970
|
+
if (process.env.OMB_OPENAI_LIVE_KEY !== undefined)
|
|
971
|
+
cfg.live.key = process.env.OMB_OPENAI_LIVE_KEY;
|
|
936
972
|
cfg.imageGen = { ...cfg.imageGen };
|
|
937
973
|
if (process.env.OMB_OPENAI_IMAGE_KEY !== undefined)
|
|
938
974
|
cfg.imageGen.key = process.env.OMB_OPENAI_IMAGE_KEY;
|
|
@@ -961,6 +997,7 @@ export function syncCredentialEnv(patch) {
|
|
|
961
997
|
const secrets = [
|
|
962
998
|
[patch.xai?.key, "XAI_API_KEY"],
|
|
963
999
|
[patch.mistral?.key, "MISTRAL_API_KEY"],
|
|
1000
|
+
[patch.cerebras?.key, "CEREBRAS_API_KEY"],
|
|
964
1001
|
[patch.anthropic?.key, "OMB_ANTHROPIC_API_KEY"],
|
|
965
1002
|
[patch.openaiCompat?.key, "OPENAI_COMPAT_API_KEY"],
|
|
966
1003
|
[patch.openai?.key, "OMB_OPENAI_API_KEY"],
|
|
@@ -973,6 +1010,7 @@ export function syncCredentialEnv(patch) {
|
|
|
973
1010
|
[patch.decider?.key, "OMB_JEV_API_KEY"],
|
|
974
1011
|
[patch.imageGen?.key, "OMB_OPENAI_IMAGE_KEY"],
|
|
975
1012
|
[patch.imageGen?.customApiKey, "OMB_CUSTOM_IMAGE_KEY"],
|
|
1013
|
+
[patch.live?.key, "OMB_OPENAI_LIVE_KEY"],
|
|
976
1014
|
];
|
|
977
1015
|
for (const [value, name] of secrets) {
|
|
978
1016
|
if (value === undefined)
|
|
@@ -1007,6 +1045,7 @@ export function syncCredentialEnv(patch) {
|
|
|
1007
1045
|
export const WORKSPACE_CREDENTIAL_ENV = [
|
|
1008
1046
|
"XAI_API_KEY",
|
|
1009
1047
|
"MISTRAL_API_KEY",
|
|
1048
|
+
"CEREBRAS_API_KEY",
|
|
1010
1049
|
"OMB_ANTHROPIC_API_KEY",
|
|
1011
1050
|
"OMB_ANTHROPIC_API_URL",
|
|
1012
1051
|
"OMB_HOSTED_MODEL_TOKEN",
|
|
@@ -1022,6 +1061,7 @@ export const WORKSPACE_CREDENTIAL_ENV = [
|
|
|
1022
1061
|
"OMB_JEV_API_KEY",
|
|
1023
1062
|
"OMB_OPENAI_IMAGE_KEY",
|
|
1024
1063
|
"OMB_CUSTOM_IMAGE_KEY",
|
|
1064
|
+
"OMB_OPENAI_LIVE_KEY",
|
|
1025
1065
|
"COMPOSIO_API_KEY",
|
|
1026
1066
|
"OMB_COMPOSIO_BROKER_TOKEN",
|
|
1027
1067
|
// Cloud Pro's included Boat, voice and decision relay tokens
|
|
@@ -1079,6 +1119,7 @@ export const PROVIDER_CREDENTIAL_ENV = [
|
|
|
1079
1119
|
"OPENCODE_API_KEY",
|
|
1080
1120
|
"XAI_API_KEY",
|
|
1081
1121
|
"MISTRAL_API_KEY",
|
|
1122
|
+
"CEREBRAS_API_KEY",
|
|
1082
1123
|
"CURSOR_API_KEY",
|
|
1083
1124
|
"CURSOR_AUTH_TOKEN",
|
|
1084
1125
|
];
|
|
@@ -1111,7 +1152,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
1111
1152
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
1112
1153
|
if (storedProfiles.success)
|
|
1113
1154
|
disk.browserProfiles = storedProfiles.data;
|
|
1114
|
-
for (const key of ["xai", "anthropic", "mistral", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1155
|
+
for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "live", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1115
1156
|
const section = checkedPatch[key];
|
|
1116
1157
|
if (!section)
|
|
1117
1158
|
continue;
|
|
@@ -1340,6 +1381,8 @@ export function instanceOwnsRouting(cfg, entry, routingDefaults) {
|
|
|
1340
1381
|
|| ownUrl(routingDefaults?.url || cfg.openaiCompat?.url || process.env.OPENAI_COMPAT_URL || "https://openrouter.ai/api/v1");
|
|
1341
1382
|
case "mistral":
|
|
1342
1383
|
return own(entry.environment?.MISTRAL_API_KEY) || ownUrl("https://api.mistral.ai/v1");
|
|
1384
|
+
case "cerebras":
|
|
1385
|
+
return own(entry.environment?.CEREBRAS_API_KEY) || ownUrl("https://api.cerebras.ai/v1");
|
|
1343
1386
|
case "grok":
|
|
1344
1387
|
return own(entry.environment?.XAI_API_KEY) || (own(config.apiKeyEnv) && config.apiKeyEnv !== "XAI_API_KEY")
|
|
1345
1388
|
|| ownUrl("https://api.x.ai/v1", cfg.xai?.url);
|
|
@@ -1357,6 +1400,8 @@ function injectedEnvironment(cfg, instanceId, driver) {
|
|
|
1357
1400
|
const environment = new Map();
|
|
1358
1401
|
if (driver === "mistral" && cfg.mistral?.key)
|
|
1359
1402
|
environment.set("MISTRAL_API_KEY", cfg.mistral.key);
|
|
1403
|
+
if (driver === "cerebras" && cfg.cerebras?.key)
|
|
1404
|
+
environment.set("CEREBRAS_API_KEY", cfg.cerebras.key);
|
|
1360
1405
|
if (driver === "grok" && cfg.xai?.key)
|
|
1361
1406
|
environment.set("XAI_API_KEY", cfg.xai.key);
|
|
1362
1407
|
// The workspace Anthropic key reaches Claude Code as the variable it
|
|
@@ -1453,6 +1498,7 @@ export function instanceConfigs(cfg) {
|
|
|
1453
1498
|
computer: { driver: "boxAgent" },
|
|
1454
1499
|
openaiCompat: { driver: "openai-compat" },
|
|
1455
1500
|
mistral: { driver: "mistral" },
|
|
1501
|
+
cerebras: { driver: "cerebras" },
|
|
1456
1502
|
...API_KEY_FLEET,
|
|
1457
1503
|
qwen: { driver: "qwenAgent" },
|
|
1458
1504
|
hermes: { driver: "hermesAgent" },
|
|
@@ -1471,6 +1517,7 @@ export function instanceConfigs(cfg) {
|
|
|
1471
1517
|
cursor: { driver: "cursorAgent" },
|
|
1472
1518
|
openaiCompat: { driver: "openai-compat" },
|
|
1473
1519
|
mistral: { driver: "mistral" },
|
|
1520
|
+
cerebras: { driver: "cerebras" },
|
|
1474
1521
|
...API_KEY_FLEET,
|
|
1475
1522
|
...CUSTOM_ONLY,
|
|
1476
1523
|
};
|
|
@@ -82,7 +82,7 @@ export function poolLocalVmTarget(seat) {
|
|
|
82
82
|
label: `pool-${seat}`,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
/** Only provisioning creates this durable directory; idle
|
|
85
|
+
/** Only provisioning creates this durable directory; idle shutdown keeps it. */
|
|
86
86
|
export function localVmWorkspaceExists(target) {
|
|
87
87
|
try {
|
|
88
88
|
return lstatSync(target.workspaceDir, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
@@ -269,6 +269,7 @@ function emptyStatus(platform, target) {
|
|
|
269
269
|
desktopReady: false,
|
|
270
270
|
desktop_error: null,
|
|
271
271
|
create_supported: true,
|
|
272
|
+
resumable: false,
|
|
272
273
|
ready: false,
|
|
273
274
|
problem: "Install a supported container runtime first",
|
|
274
275
|
image_ref: IMAGE,
|
|
@@ -283,21 +284,8 @@ function emptyStatus(platform, target) {
|
|
|
283
284
|
viewer_url: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
|
-
/**
|
|
287
|
-
*
|
|
288
|
-
* True for exactly one state: the container is gone, and a plain `run` is all
|
|
289
|
-
* that is needed to bring it back. That is what `LocalVmIdleTimer` leaves
|
|
290
|
-
* behind — it removes an unused Local VM rather than pausing it — so a turn
|
|
291
|
-
* arriving after an idle period should not have to send the person to App
|
|
292
|
-
* Settings for a container the app itself deleted.
|
|
293
|
-
*
|
|
294
|
-
* Every other problem in `statusProblem` stays the person's call and returns
|
|
295
|
-
* false here: no runtime, daemon down, image never prepared, `create_supported`
|
|
296
|
-
* false, and any existing container — stale image, unmanaged, unsafe network,
|
|
297
|
-
* security or persistence. A stopped container is excluded deliberately, since
|
|
298
|
-
* `statusProblem` says this desktop image cannot safely resume and asks for a
|
|
299
|
-
* recreate rather than a start.
|
|
300
|
-
*/
|
|
287
|
+
/** Recreate a missing desktop when its image and runtime are already prepared.
|
|
288
|
+
* This also recovers desktops deleted by older versions' idle cleanup. */
|
|
301
289
|
export function localVmRecreatableOnDemand(status) {
|
|
302
290
|
return Boolean(status.runtime)
|
|
303
291
|
&& status.daemonUp
|
|
@@ -305,12 +293,28 @@ export function localVmRecreatableOnDemand(status) {
|
|
|
305
293
|
&& status.container === "missing"
|
|
306
294
|
&& status.create_supported;
|
|
307
295
|
}
|
|
296
|
+
/** Start only an existing, compatible desktop with the managed safety boundary.
|
|
297
|
+
* Fails closed on fields left "unknown" by a partial inspect, which
|
|
298
|
+
* `statusProblem` alone would let through. */
|
|
299
|
+
export function localVmResumable(status) {
|
|
300
|
+
return Boolean(status.runtime) && status.daemonUp && status.image && status.container === "stopped"
|
|
301
|
+
&& existingContainerProblem(status) === null
|
|
302
|
+
&& status.network === "loopback" && status.security === "hardened" && status.persistence === "durable";
|
|
303
|
+
}
|
|
304
|
+
/** What a turn may do on its own to bring this Local VM up, if anything. */
|
|
305
|
+
export function localVmWakeAction(status) {
|
|
306
|
+
if (localVmRecreatableOnDemand(status))
|
|
307
|
+
return "run";
|
|
308
|
+
if (localVmResumable(status))
|
|
309
|
+
return "start";
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
308
312
|
/** Whether Auto may attach this Local VM without a person choosing it: the
|
|
309
|
-
* desktop is ready,
|
|
310
|
-
*
|
|
313
|
+
* desktop is ready, a compatible stopped desktop can be started, or its image
|
|
314
|
+
* is prepared and a missing container can be recreated. Anything else — no runtime, daemon down, image
|
|
311
315
|
* never prepared, an unmanaged or unsafe container — stays the person's call. */
|
|
312
316
|
export function autoLocalVmAttachable(status) {
|
|
313
|
-
return status.ready === true ||
|
|
317
|
+
return status.ready === true || localVmWakeAction(status) !== null;
|
|
314
318
|
}
|
|
315
319
|
function statusProblem(status) {
|
|
316
320
|
if (!status.runtime)
|
|
@@ -324,6 +328,19 @@ function statusProblem(status) {
|
|
|
324
328
|
}
|
|
325
329
|
if (status.container === "missing")
|
|
326
330
|
return "Create the Local VM";
|
|
331
|
+
const existing = existingContainerProblem(status);
|
|
332
|
+
if (existing)
|
|
333
|
+
return existing;
|
|
334
|
+
if (status.container === "stopped")
|
|
335
|
+
return "The Local VM is stopped; start it to continue";
|
|
336
|
+
if (status.desktop_error)
|
|
337
|
+
return `The Local VM desktop failed to start: ${status.desktop_error}`;
|
|
338
|
+
if (!status.desktopReady)
|
|
339
|
+
return "The Local VM started, but Cua Driver is not ready yet";
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
/** Problems with an existing container that only a recreate fixes. */
|
|
343
|
+
function existingContainerProblem(status) {
|
|
327
344
|
if (!status.imageMatches)
|
|
328
345
|
return "The existing Local VM uses an older desktop or Cua Driver; recreate it";
|
|
329
346
|
if (!status.managed)
|
|
@@ -334,12 +351,6 @@ function statusProblem(status) {
|
|
|
334
351
|
return "The existing Local VM is missing safety limits; recreate it";
|
|
335
352
|
if (status.persistence === "unsafe")
|
|
336
353
|
return "The existing Local VM is missing its durable folder; recreate it";
|
|
337
|
-
if (status.container === "stopped")
|
|
338
|
-
return "This desktop image cannot safely resume; recreate the Local VM";
|
|
339
|
-
if (status.desktop_error)
|
|
340
|
-
return `The Local VM desktop failed to start: ${status.desktop_error}`;
|
|
341
|
-
if (!status.desktopReady)
|
|
342
|
-
return "The Local VM started, but Cua Driver is not ready yet";
|
|
343
354
|
return null;
|
|
344
355
|
}
|
|
345
356
|
/** Shared with the BYO-VPS backend (vps-computer.ts): both containers are
|
|
@@ -457,6 +468,7 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
|
|
|
457
468
|
const inspected = JSON.parse(stdout);
|
|
458
469
|
const detail = inspected[0];
|
|
459
470
|
status.container = detail?.State?.Running ? "running" : "stopped";
|
|
471
|
+
status.stopped_at = status.container === "stopped" ? detail?.State?.FinishedAt ?? null : null;
|
|
460
472
|
status.network = dockerPortsAreLocal(detail?.HostConfig?.PortBindings) ? "loopback" : "unsafe";
|
|
461
473
|
status.viewer_port = dockerViewerPort(detail?.NetworkSettings?.Ports, target.viewerPort);
|
|
462
474
|
status.imageMatches =
|
|
@@ -530,6 +542,7 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
|
|
|
530
542
|
}
|
|
531
543
|
status.problem = statusProblem(status);
|
|
532
544
|
status.ready = status.problem === null;
|
|
545
|
+
status.resumable = localVmResumable(status);
|
|
533
546
|
return status;
|
|
534
547
|
}
|
|
535
548
|
function loopback(address) {
|
|
@@ -597,8 +610,8 @@ function appleWorkspaceMountIsSafe(mounts, platform, expectedWorkspace) {
|
|
|
597
610
|
* runtime-specific capability exception is Podman's Firefox sandbox chroot.
|
|
598
611
|
* Callers also differ on restart policy — the VPS
|
|
599
612
|
* container must survive a reboot nobody is watching ("unless-stopped"),
|
|
600
|
-
* while
|
|
601
|
-
*
|
|
613
|
+
* while Local VM starts remain controlled by OMB's idle policy and turn
|
|
614
|
+
* lifecycle rather than a daemon restart policy. */
|
|
602
615
|
export function dockerSecurityIsHardened(config, options = {}) {
|
|
603
616
|
if (!config)
|
|
604
617
|
return false;
|
|
@@ -727,10 +740,8 @@ export async function containerComputerAction(action, runner = sh, platform = pr
|
|
|
727
740
|
if (action === "run" && !before.create_supported) {
|
|
728
741
|
throw Object.assign(new Error(before.problem ?? "This runtime cannot create a per-bot Local VM"), { status: 409 });
|
|
729
742
|
}
|
|
730
|
-
if (action === "start") {
|
|
731
|
-
throw Object.assign(new Error(
|
|
732
|
-
status: 409,
|
|
733
|
-
});
|
|
743
|
+
if (action === "start" && !localVmResumable(before)) {
|
|
744
|
+
throw Object.assign(new Error(before.problem ?? "The Local VM is not stopped"), { status: 409 });
|
|
734
745
|
}
|
|
735
746
|
if (action === "stop" && before.container !== "running") {
|
|
736
747
|
throw Object.assign(new Error("The Local VM is not running"), { status: 409 });
|
|
@@ -943,7 +954,7 @@ export function setupCommands(runtime, platform = process.platform, target = SHA
|
|
|
943
954
|
run: runtime === "container" && target.key !== SHARED_LOCAL_VM_TARGET.key
|
|
944
955
|
? null
|
|
945
956
|
: command(containerRunArgs(runtime, "CHANGE_ME", target)),
|
|
946
|
-
start:
|
|
957
|
+
start: command(["start", target.containerName]),
|
|
947
958
|
stop: command(["stop", target.containerName]),
|
|
948
959
|
remove: command(["rm", runtime === "container" ? "--force" : "-f", target.containerName]),
|
|
949
960
|
view: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
|
|
@@ -65,9 +65,10 @@ export function decisionRetentionDays(configured, env = process.env) {
|
|
|
65
65
|
}
|
|
66
66
|
const actorScope = new AsyncLocalStorage();
|
|
67
67
|
/** Run `work` as a person's card answer: every `source: "user"` row it
|
|
68
|
-
* writes names `actor`,
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
* writes names `actor` (and `via`, for an answer spoken on a Live call),
|
|
69
|
+
* without each resolver having to thread it through. */
|
|
70
|
+
export function withDecisionActor(actor, work, via) {
|
|
71
|
+
return actorScope.run(via ? { actor, via } : { actor }, work);
|
|
71
72
|
}
|
|
72
73
|
function monthKey(at) {
|
|
73
74
|
return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
@@ -88,10 +89,12 @@ async function writeDecision(dataDir, record) {
|
|
|
88
89
|
* the fold that calls this is delivering approvals and cards, and a full
|
|
89
90
|
* disk must not turn into denied tools. */
|
|
90
91
|
export function appendDecision(dataDir, row) {
|
|
91
|
-
const
|
|
92
|
+
const scope = row.source === "user" ? actorScope.getStore() : undefined;
|
|
93
|
+
const actor = row.actor ?? scope?.actor;
|
|
94
|
+
const via = row.via ?? scope?.via;
|
|
92
95
|
// Redact now, not when the queue drains: the row is what was true at the
|
|
93
96
|
// moment of the decision.
|
|
94
|
-
const record = redactSecrets({ at: new Date().toISOString(), ...row, ...(actor ? { actor } : {}) });
|
|
97
|
+
const record = redactSecrets({ at: new Date().toISOString(), ...row, ...(actor ? { actor } : {}), ...(via ? { via } : {}) });
|
|
95
98
|
const previous = writeQueues.get(dataDir) ?? Promise.resolve();
|
|
96
99
|
// Serialize appends (and the occasional prune) per directory, so two
|
|
97
100
|
// simultaneous approvals keep their decision order.
|
|
@@ -258,7 +261,7 @@ export function decisionsCsv(rows) {
|
|
|
258
261
|
row.summary ?? "",
|
|
259
262
|
row.rule ?? "",
|
|
260
263
|
row.unattended ? "yes" : "",
|
|
261
|
-
actorLabel(row.actor),
|
|
264
|
+
row.via === "call" ? `${actorLabel(row.actor)} (by voice)` : actorLabel(row.actor),
|
|
262
265
|
row.threadId,
|
|
263
266
|
row.requestId ?? "",
|
|
264
267
|
].map(csvCell).join(","));
|
|
@@ -192,6 +192,8 @@ export function _loadPending() {
|
|
|
192
192
|
if (typeof item.targetThreadId === "string" && item.targetThreadId) {
|
|
193
193
|
loaded.targetThreadId = item.targetThreadId;
|
|
194
194
|
}
|
|
195
|
+
if (item.oneWay === true)
|
|
196
|
+
loaded.oneWay = true;
|
|
195
197
|
return [loaded];
|
|
196
198
|
});
|
|
197
199
|
if (items.length)
|
|
@@ -268,7 +270,7 @@ const MAX_QUEUED_PER_THREAD = 6;
|
|
|
268
270
|
export function queueDelegation(bus, from, item, maxDepth, sourceThreadId = from.threadId) {
|
|
269
271
|
if (item.toBotId === from.id)
|
|
270
272
|
return { result: "self" };
|
|
271
|
-
if (item.depth >= maxDepth)
|
|
273
|
+
if (!item.oneWay && item.depth >= maxDepth)
|
|
272
274
|
return { result: "too_deep" };
|
|
273
275
|
const target = bus.store.bot(item.toBotId);
|
|
274
276
|
if (!target)
|
|
@@ -347,6 +349,11 @@ onSettled) {
|
|
|
347
349
|
const from = bus.store.botByThread(threadId) ??
|
|
348
350
|
bus.store.bot(item.sourceBotId);
|
|
349
351
|
if (!from) {
|
|
352
|
+
appendDeliveryMessage(bus, threadId, item, {
|
|
353
|
+
role: "bot",
|
|
354
|
+
kind: "activity",
|
|
355
|
+
tool: { name: "Send failed — the sending bot no longer exists", ok: false },
|
|
356
|
+
});
|
|
350
357
|
recordDelegationReceipt({
|
|
351
358
|
id: item.id,
|
|
352
359
|
sourceThreadId: threadId,
|
|
@@ -373,7 +380,7 @@ onSettled) {
|
|
|
373
380
|
result: why.slice(0, 200),
|
|
374
381
|
});
|
|
375
382
|
try {
|
|
376
|
-
bus
|
|
383
|
+
appendDeliveryMessage(bus, threadId, item, {
|
|
377
384
|
role: "bot",
|
|
378
385
|
kind: "activity",
|
|
379
386
|
tool: { name: `error: delegation failed — ${why.slice(0, 120)}`, ok: false },
|
|
@@ -393,7 +400,7 @@ onSettled) {
|
|
|
393
400
|
if (outcome === "settled" && stillQueued) {
|
|
394
401
|
const receipt = findDelegationReceipt(item.id);
|
|
395
402
|
try {
|
|
396
|
-
if (receipt)
|
|
403
|
+
if (receipt && !item.oneWay)
|
|
397
404
|
onSettled?.(receipt);
|
|
398
405
|
}
|
|
399
406
|
catch (error) {
|
|
@@ -428,6 +435,15 @@ function acknowledgeDelegation(threadId, itemId) {
|
|
|
428
435
|
pendingDelegations.delete(threadId);
|
|
429
436
|
savePending();
|
|
430
437
|
}
|
|
438
|
+
function appendDeliveryMessage(bus, sourceThreadId, item, message) {
|
|
439
|
+
const threadId = item.oneWay && item.targetThreadId && bus.store.taskByThread(item.toBotId, item.targetThreadId)
|
|
440
|
+
? item.targetThreadId : sourceThreadId;
|
|
441
|
+
// A failed independent send needs a visible receipt, not a source wake.
|
|
442
|
+
// Never recreate a deleted conversation just to post that failure.
|
|
443
|
+
if (item.oneWay && threadId === sourceThreadId && !sourceThreadBelongsToBot(bus.store, item.sourceBotId, sourceThreadId))
|
|
444
|
+
return;
|
|
445
|
+
bus.store.appendMessage(threadId, message);
|
|
446
|
+
}
|
|
431
447
|
const isExpired = (item, now) => now - item.queuedAt >= DELEGATION_TTL_MS;
|
|
432
448
|
/** Past the busy-hold cap — the tighter bound that fires while its target is
|
|
433
449
|
* still busy, long before the 24-hour window. */
|
|
@@ -457,9 +473,9 @@ function expireDelegation(bus, sourceThreadId, item, ownerId) {
|
|
|
457
473
|
status: "expired",
|
|
458
474
|
result: busyHold ? `@${name} was still busy after ${busyHoldCapText()}` : `@${name} was not free to take this for 24 hours`,
|
|
459
475
|
});
|
|
460
|
-
if (!sourceThreadBelongsToBot(bus.store, ownerId, sourceThreadId))
|
|
476
|
+
if (!item.oneWay && !sourceThreadBelongsToBot(bus.store, ownerId, sourceThreadId))
|
|
461
477
|
return;
|
|
462
|
-
bus
|
|
478
|
+
appendDeliveryMessage(bus, sourceThreadId, item, {
|
|
463
479
|
role: "bot",
|
|
464
480
|
kind: "activity",
|
|
465
481
|
tool: {
|
|
@@ -511,15 +527,16 @@ export function expireStaleDelegations(bus, now, onSettled) {
|
|
|
511
527
|
expireDelegation(bus, threadId, item, ownerId ?? item.sourceBotId);
|
|
512
528
|
const receipt = findDelegationReceipt(item.id);
|
|
513
529
|
if (receipt)
|
|
514
|
-
expired.push(receipt);
|
|
530
|
+
expired.push({ receipt, oneWay: item.oneWay === true });
|
|
515
531
|
}
|
|
516
532
|
}
|
|
517
533
|
if (!expired.length)
|
|
518
534
|
return 0;
|
|
519
535
|
savePending();
|
|
520
|
-
for (const receipt of expired) {
|
|
536
|
+
for (const { receipt, oneWay } of expired) {
|
|
521
537
|
try {
|
|
522
|
-
|
|
538
|
+
if (!oneWay)
|
|
539
|
+
onSettled?.(receipt);
|
|
523
540
|
}
|
|
524
541
|
catch (error) {
|
|
525
542
|
console.error("delegation expired but its source could not be resumed", error);
|
|
@@ -533,9 +550,14 @@ export function discardDelegations(bus, threadId) {
|
|
|
533
550
|
const list = pendingDelegations.get(threadId);
|
|
534
551
|
if (!list?.length)
|
|
535
552
|
return;
|
|
536
|
-
|
|
553
|
+
const kept = list.filter(item => item.oneWay);
|
|
554
|
+
const dropped = list.filter(item => !item.oneWay);
|
|
555
|
+
if (kept.length)
|
|
556
|
+
pendingDelegations.set(threadId, kept);
|
|
557
|
+
else
|
|
558
|
+
pendingDelegations.delete(threadId);
|
|
537
559
|
savePending();
|
|
538
|
-
for (const item of
|
|
560
|
+
for (const item of dropped) {
|
|
539
561
|
recordDelegationReceipt({
|
|
540
562
|
id: item.id,
|
|
541
563
|
sourceThreadId: threadId,
|
|
@@ -545,14 +567,16 @@ export function discardDelegations(bus, threadId) {
|
|
|
545
567
|
result: "the delegating turn did not finish",
|
|
546
568
|
});
|
|
547
569
|
}
|
|
570
|
+
if (!dropped.length)
|
|
571
|
+
return;
|
|
548
572
|
const from = bus.store.botByThread(threadId) ??
|
|
549
|
-
bus.store.bot(
|
|
573
|
+
bus.store.bot(dropped.find((item) => bus.store.bot(item.sourceBotId))?.sourceBotId ?? dropped[0].sourceBotId);
|
|
550
574
|
if (!from)
|
|
551
575
|
return;
|
|
552
576
|
bus.store.appendMessage(threadId, {
|
|
553
577
|
role: "bot",
|
|
554
578
|
kind: "activity",
|
|
555
|
-
tool: { name: `${
|
|
579
|
+
tool: { name: `${dropped.length} queued delegation${dropped.length > 1 ? "s" : ""} dropped — the turn did not finish`, ok: false },
|
|
556
580
|
});
|
|
557
581
|
}
|
|
558
582
|
async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarget) {
|
|
@@ -560,7 +584,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
560
584
|
let target = bus.store.bot(item.toBotId);
|
|
561
585
|
// Retained approval authorizes the message, not a deleted conversation or
|
|
562
586
|
// revoked room membership. Check every retry before writing to its source.
|
|
563
|
-
if (!sourceThreadBelongsToBot(bus.store, sender.id, sourceThreadId)) {
|
|
587
|
+
if (!item.oneWay && !sourceThreadBelongsToBot(bus.store, sender.id, sourceThreadId)) {
|
|
564
588
|
recordDelegationReceipt({
|
|
565
589
|
id: item.id,
|
|
566
590
|
sourceThreadId,
|
|
@@ -580,7 +604,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
580
604
|
status: "error",
|
|
581
605
|
result: "no such bot",
|
|
582
606
|
});
|
|
583
|
-
bus
|
|
607
|
+
appendDeliveryMessage(bus, sourceThreadId, item, {
|
|
584
608
|
role: "bot",
|
|
585
609
|
kind: "activity",
|
|
586
610
|
tool: { name: `error: delegation to ${item.toBotId} failed — no such bot`, ok: false },
|
|
@@ -621,7 +645,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
621
645
|
// a denial, which otherwise recreates a deleted source transcript.
|
|
622
646
|
const current = bus.store.bot(item.toBotId);
|
|
623
647
|
const currentSender = bus.store.bot(from.id);
|
|
624
|
-
if (!current || !currentSender || !sourceThreadBelongsToBot(bus.store, currentSender.id, sourceThreadId)) {
|
|
648
|
+
if (!current || !currentSender || (!item.oneWay && !sourceThreadBelongsToBot(bus.store, currentSender.id, sourceThreadId))) {
|
|
625
649
|
recordDelegationReceipt({
|
|
626
650
|
id: item.id,
|
|
627
651
|
sourceThreadId,
|
|
@@ -644,7 +668,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
644
668
|
approvalOutcome: verdict,
|
|
645
669
|
result: verdict === "deny" ? "the user denied this handoff" : failure.error,
|
|
646
670
|
});
|
|
647
|
-
bus
|
|
671
|
+
appendDeliveryMessage(bus, sourceThreadId, item, {
|
|
648
672
|
role: "bot",
|
|
649
673
|
kind: "activity",
|
|
650
674
|
tool: { name: verdict === "deny" ? `Delegation to @${target.name} denied by user`
|
|
@@ -682,8 +706,9 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
682
706
|
// members; otherwise the exchange falls back to the pair DM.
|
|
683
707
|
const originatingGroup = (item.originatingGroupId ? bus.store.group(item.originatingGroupId) : undefined) ??
|
|
684
708
|
(sourceThreadId ? bus.store.groupByThread(sourceThreadId) : undefined);
|
|
685
|
-
const channel = getOrCreateChannel(bus.store, sender, target, originatingGroup);
|
|
686
|
-
|
|
709
|
+
const channel = item.oneWay ? undefined : getOrCreateChannel(bus.store, sender, target, originatingGroup);
|
|
710
|
+
if (channel)
|
|
711
|
+
mirrorExchange(bus, sender, target, item.message, channel, sourceThreadId);
|
|
687
712
|
const reasonLine = item.reason ? `\n\n[Reason: ${item.reason}]` : "";
|
|
688
713
|
// A fresh thread's first line gets the shared peer-provenance note from
|
|
689
714
|
// the harness (which knows whether the opener was unattended); the
|
|
@@ -691,7 +716,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
691
716
|
const prefixed = item.targetThreadId
|
|
692
717
|
? item.message
|
|
693
718
|
: `[Delegated by @${sender.name}, another bot in this OpenMausBot workspace. Do the work and reply directly.]\n\n${item.message}${reasonLine}`;
|
|
694
|
-
await runTarget(item.toBotId, prefixed, item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId);
|
|
719
|
+
await runTarget(item.toBotId, prefixed, item.oneWay ? 0 : item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId, item.oneWay === true);
|
|
695
720
|
return "dispatched";
|
|
696
721
|
}
|
|
697
722
|
/** "Is the target free to take this handoff right now?" Both shapes ask for
|
|
@@ -723,7 +748,7 @@ function holdWhileTargetBusy(bus, target, sourceThreadId, item, canTakeTurn = ta
|
|
|
723
748
|
item.busySince = Date.now();
|
|
724
749
|
if (!item.waitAnnounced) {
|
|
725
750
|
item.waitAnnounced = true;
|
|
726
|
-
bus
|
|
751
|
+
appendDeliveryMessage(bus, sourceThreadId, item, {
|
|
727
752
|
role: "bot",
|
|
728
753
|
kind: "activity",
|
|
729
754
|
tool: { name: waitingChipText(bus.store, target, item) },
|
|
@@ -757,7 +782,7 @@ function dropIfThreadGone(bus, target, sourceThreadId, item) {
|
|
|
757
782
|
status: "dropped",
|
|
758
783
|
result: `the thread opened on @${target.name} was deleted before it could start`,
|
|
759
784
|
});
|
|
760
|
-
bus
|
|
785
|
+
appendDeliveryMessage(bus, sourceThreadId, item, {
|
|
761
786
|
role: "bot",
|
|
762
787
|
kind: "activity",
|
|
763
788
|
tool: { name: `Thread on @${target.name} canceled — it was deleted before it could start`, ok: false },
|
|
@@ -795,7 +820,7 @@ function dropIfUnreachable(bus, sender, target, sourceThreadId, item) {
|
|
|
795
820
|
status: "dropped",
|
|
796
821
|
result,
|
|
797
822
|
});
|
|
798
|
-
bus
|
|
823
|
+
appendDeliveryMessage(bus, sourceThreadId, item, {
|
|
799
824
|
role: "bot",
|
|
800
825
|
kind: "activity",
|
|
801
826
|
tool: { name: `Delegation to @${target.name} canceled — ${reason}`, ok: false },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toolIdentity } from "../shared/tool-name.js";
|
|
1
2
|
/** Evidence is observed, not inferred from a provider's brand. In particular,
|
|
2
3
|
* OpenAI-compatible and Grok turns now expose structured tool activity too. */
|
|
3
4
|
export function coverageForDriver(_driverKind, fullResults = false, observedTools = false) {
|
|
@@ -20,13 +21,19 @@ export function buildTurnDigest(input) {
|
|
|
20
21
|
for (const m of input.activities) {
|
|
21
22
|
if (m.kind !== "activity" || !m.tool?.itemId || m.turnId !== input.turnId)
|
|
22
23
|
continue;
|
|
23
|
-
|
|
24
|
+
// by identity, not by the chip's title: a command-titled chip would
|
|
25
|
+
// otherwise be its own bucket and carry the unredacted command line into
|
|
26
|
+
// a row that is FTS-indexed and replayed into rebuilt context
|
|
27
|
+
const identity = toolIdentity(m.tool.name);
|
|
28
|
+
const entry = byName.get(identity) ?? { name: fitBytes(identity, 160), count: 0, failed: 0 };
|
|
24
29
|
entry.count += 1;
|
|
25
30
|
if (m.tool.ok === false)
|
|
26
31
|
entry.failed += 1;
|
|
32
|
+
// the driver's summary is the bounded, redacted one-line command; the
|
|
33
|
+
// title is neither, so a shell bucket says what it ran only through this
|
|
27
34
|
if (entry.sample === undefined && m.tool.summary)
|
|
28
35
|
entry.sample = fitBytes(m.tool.summary.replace(/\s+/g, " "), 300);
|
|
29
|
-
byName.set(
|
|
36
|
+
byName.set(identity, entry);
|
|
30
37
|
}
|
|
31
38
|
// busiest first; ties keep first-seen order (Map preserves insertion)
|
|
32
39
|
const ranked = [...byName.values()].sort((a, b) => b.count - a.count);
|
|
@@ -78,6 +85,9 @@ function firstSentence(reply) {
|
|
|
78
85
|
export function renderDigest(d) {
|
|
79
86
|
const parts = ["[digest]"];
|
|
80
87
|
if (d.tools.length) {
|
|
88
|
+
// every entry ends at "×N" or "(N failed)": the phones split the tools
|
|
89
|
+
// part on exactly that (ios DigestSummary, android TurnDigest), so a
|
|
90
|
+
// shell bucket's sample stays in the stored digest, not in this line
|
|
81
91
|
const tools = d.tools.map((t) => `${t.name} ×${t.count}${t.failed ? ` (${t.failed} failed)` : ""}`).join(", ");
|
|
82
92
|
parts.push(`tools: ${tools}${d.toolsDropped ? ` +${d.toolsDropped} more` : ""}${d.hookCoverage === "preview" ? " (from tool previews)" : ""}`);
|
|
83
93
|
}
|