openmausbot 0.1.89 → 0.1.91
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/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
- package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
- package/dist/assets/channel-DrfGMu7X.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
- package/dist/assets/index-BlnCEcU6.js +588 -0
- package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
- package/dist/assets/index-DVxjOvID.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
- package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
- package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +14810 -0
- package/dist-server/container-mcp.js +136 -36
- package/dist-server/drivers/agents-proxy.js +31 -10
- package/dist-server/index.js +48251 -44124
- package/dist-server/openmausbot.js +628 -292
- package/dist-server/pair-cli.js +628 -292
- package/dist-server/permission-proxy.js +1 -1
- package/dist-server/server/admission.js +46 -0
- package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
- package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
- package/dist-server/server/{box.js → boat.js} +381 -334
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/bot-profile.js +7 -1
- package/dist-server/server/channel-queue.js +58 -18
- package/dist-server/server/checkpoints.js +98 -52
- package/dist-server/server/claim-idle.js +33 -0
- package/dist-server/server/cloud-backend.js +5 -5
- package/dist-server/server/cloud-home-start.js +121 -0
- package/dist-server/server/cloud-home.js +241 -0
- package/dist-server/server/cloud-lending.js +103 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/cloud-overflow.js +167 -0
- package/dist-server/server/composio.js +5 -0
- package/dist-server/server/computer-observation.js +2 -2
- package/dist-server/server/config.js +156 -9
- package/dist-server/server/container-computer.js +13 -0
- package/dist-server/server/control-client.js +2 -2
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/delegations.js +11 -3
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/agents-call.js +5 -1
- package/dist-server/server/drivers/agents-catalog.js +25 -3
- package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
- package/dist-server/server/drivers/builtIn.js +2 -2
- package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
- package/dist-server/server/drivers/claude.js +178 -13
- package/dist-server/server/drivers/native.js +1 -1
- package/dist-server/server/drivers/openai-chat.js +15 -4
- package/dist-server/server/drivers/pi.js +3 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/hosted-models.js +6 -1
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1462 -576
- package/dist-server/server/local-vm-inventory.js +11 -1
- package/dist-server/server/local-vm-seat-pool.js +141 -0
- package/dist-server/server/managed-policy.js +4 -1
- package/dist-server/server/memory-capture.js +194 -0
- package/dist-server/server/memory-entries.js +76 -0
- package/dist-server/server/memory-organize.js +102 -0
- package/dist-server/server/memory-tidy.js +144 -0
- package/dist-server/server/memory-topics.js +169 -0
- package/dist-server/server/memory-upkeep.js +452 -0
- package/dist-server/server/message-db.js +49 -5
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/permission-proxy.js +3 -3
- package/dist-server/server/profile-learned.js +98 -0
- package/dist-server/server/profile-versions.js +45 -11
- package/dist-server/server/recall.js +172 -0
- package/dist-server/server/redact.js +1 -1
- package/dist-server/server/request-auth.js +2 -0
- package/dist-server/server/room-cwd.js +1 -1
- package/dist-server/server/room-handoffs.js +61 -6
- package/dist-server/server/routes/bot-memory.js +215 -0
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routine-requests.js +27 -16
- package/dist-server/server/routines.js +10 -6
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/steer-queue.js +41 -10
- package/dist-server/server/store.js +42 -3
- package/dist-server/server/surface.js +14 -1
- package/dist-server/server/system-prompt.js +16 -5
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +2 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/tightening-requests.js +34 -4
- package/dist-server/server/tool-summary.js +2 -2
- package/dist-server/server/tts/elevenlabs.js +12 -7
- package/dist-server/server/tts/index.js +17 -10
- package/dist-server/server/turn-resources.js +87 -11
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup-policy.js +22 -3
- package/dist-server/server/workspace-backup.js +25 -8
- package/dist-server/server/workspace.js +57 -22
- package/dist-server/shared/ask-question.js +7 -5
- package/dist-server/shared/bot-avatar.js +19 -0
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/credential-request.js +3 -3
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/shared/tool-surface.js +1 -1
- package/dist-server/src/lib/composer-attachments.js +9 -0
- package/dist-server/vps-container-mcp.js +136 -36
- package/dist-server/workspace-backup.worker.js +165 -35
- package/package.json +1 -1
- package/dist/assets/channel-DqY_gMLg.js +0 -1
- package/dist/assets/index-BIMdIbaw.css +0 -1
- package/dist/assets/index-BVPdbq2r.js +0 -583
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
|
@@ -13,6 +13,8 @@ import { isModelVariant } from "./contracts.js";
|
|
|
13
13
|
import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-icon.js";
|
|
14
14
|
import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
|
|
15
15
|
import { parseJson, schemaIssue } from "./schema.js";
|
|
16
|
+
import { CLOUD_SEAT_IDLE_STOP_MS } from "./cloud-overflow.js";
|
|
17
|
+
import { cloudHomeConfigured } from "./cloud-home.js";
|
|
16
18
|
const optionalText = z.string().optional();
|
|
17
19
|
const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
18
20
|
const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
@@ -87,7 +89,8 @@ const roomConfigSchema = z.object({
|
|
|
87
89
|
.min(MIN_ROOM_TURN_TIMEOUT_MINUTES)
|
|
88
90
|
.max(MAX_ROOM_TURN_TIMEOUT_MINUTES),
|
|
89
91
|
/** Room handoff tree lifetime. Active execution pauses this clock; the
|
|
90
|
-
* hard cap is
|
|
92
|
+
* hard cap is the maximum stall window, measured wall-clock from the
|
|
93
|
+
* tree's last durable progress. */
|
|
91
94
|
handoffLifetimeMinutes: z.number().int().min(1).max(MAX_ROOM_TURN_TIMEOUT_MINUTES).optional(),
|
|
92
95
|
handoffMinRunwayMinutes: z.number().int().min(1).max(MAX_ROOM_TURN_TIMEOUT_MINUTES).optional(),
|
|
93
96
|
handoffHardCapMinutes: z.number().int().min(1).max(7 * MAX_ROOM_TURN_TIMEOUT_MINUTES).optional(),
|
|
@@ -95,8 +98,14 @@ const roomConfigSchema = z.object({
|
|
|
95
98
|
(rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) &&
|
|
96
99
|
(rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) <=
|
|
97
100
|
(rooms.handoffHardCapMinutes ?? DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES), { message: "rooms handoff bounds must satisfy handoffMinRunwayMinutes <= handoffLifetimeMinutes <= handoffHardCapMinutes" });
|
|
101
|
+
/** Isolation for bot desktops. Migration note (issue #1654): switching
|
|
102
|
+
* modes changes lease keys and vm-home directories, so desktops cold-start
|
|
103
|
+
* under the new mode — a pool seat lives in vm-homes/pool-N — while the old
|
|
104
|
+
* mode's workspaces stay on disk until removed. maxInstances caps per-bot
|
|
105
|
+
* desktops in per-bot mode, or the pool's seat count in pool mode; it is
|
|
106
|
+
* not a total across modes. Default stays "shared". */
|
|
98
107
|
const localVmConfigSchema = z.object({
|
|
99
|
-
mode: z.enum(["shared", "per-bot"]).optional(),
|
|
108
|
+
mode: z.enum(["shared", "per-bot", "pool"]).optional(),
|
|
100
109
|
maxInstances: z
|
|
101
110
|
.number()
|
|
102
111
|
.int()
|
|
@@ -247,6 +256,10 @@ const featureConfigSchema = z.object({
|
|
|
247
256
|
skillAuthoring: z.boolean().optional(),
|
|
248
257
|
/** Show each tool run in the transcript. Off unless explicitly enabled. */
|
|
249
258
|
showToolCalls: z.boolean().optional(),
|
|
259
|
+
/** Run a routine inside the conversation it reports to, instead of a hidden
|
|
260
|
+
* thread. Off unless explicitly enabled. The run still starts from its own
|
|
261
|
+
* instructions, but the messages stay in that chat. */
|
|
262
|
+
routinesInConversation: z.boolean().optional(),
|
|
250
263
|
/** Experimental built-in browser. Off until explicitly enabled; each bot
|
|
251
264
|
* also has its own switch. */
|
|
252
265
|
browser: z.boolean().optional(),
|
|
@@ -260,8 +273,23 @@ const featureConfigSchema = z.object({
|
|
|
260
273
|
claudeUserMcp: z.boolean().optional(),
|
|
261
274
|
/** LLM-generated titles for new bot threads. Off until explicitly
|
|
262
275
|
* enabled; a one-shot that fails or answers junk leaves the first-message
|
|
263
|
-
|
|
276
|
+
* snippet in place — see llmThreadTitlesEnabled. */
|
|
264
277
|
llmThreadTitles: z.boolean().optional(),
|
|
278
|
+
/** Before each turn, passages from the bot's own memory files and earlier
|
|
279
|
+
* conversations that share words with the message ride into the turn.
|
|
280
|
+
* Read-only; on unless explicitly switched off — see autoRecallEnabled. */
|
|
281
|
+
autoRecall: z.boolean().optional(),
|
|
282
|
+
/** Idle release for computer claims (#1653): a desktop seat that stays
|
|
283
|
+
* screen-quiet for 90 seconds is released to waiting turns while its
|
|
284
|
+
* holder's turn still lives; the previous holder re-claims directly
|
|
285
|
+
* for 10 minutes and yields to an occupied seat. Off until baked; see
|
|
286
|
+
* computerClaimIdleReleaseEnabled for how to enable it by hand. */
|
|
287
|
+
computerClaimIdleRelease: z.boolean().optional(),
|
|
288
|
+
/** Consented cloud overflow for local computer waits (#1655): a wait
|
|
289
|
+
* may offer a per-second-billed cloud seat, but only through an explicit
|
|
290
|
+
* consent card or a configured allowlist — never a silent Auto default.
|
|
291
|
+
* Off until baked; see cloudOverflowEnabled for how to enable it by hand. */
|
|
292
|
+
cloudOverflow: z.boolean().optional(),
|
|
265
293
|
});
|
|
266
294
|
/** First-run progress. Kept in the workspace config rather than a browser so
|
|
267
295
|
* it survives cleared site data and is shared by every paired client. Hint
|
|
@@ -376,6 +404,8 @@ const appConfigSchema = z.object({
|
|
|
376
404
|
/** Project key used for Sessions, catalog and agent tools. userId/sessionId
|
|
377
405
|
* are non-secret local identifiers used to reuse one Composio Session. */
|
|
378
406
|
composio: z.object({ apiKey: optionalText, userId: optionalText, sessionId: optionalText }).optional(),
|
|
407
|
+
/** Historical config section name "box" (the persisted config.json key); the
|
|
408
|
+
* provider is Boat now and the key is kept for compatibility. */
|
|
379
409
|
box: z.object({ token: optionalText }).optional(),
|
|
380
410
|
vps: vpsConfigSchema.optional(),
|
|
381
411
|
/** Optional OpenCode key; persisted write-only and passed only to its child. */
|
|
@@ -400,6 +430,24 @@ const appConfigSchema = z.object({
|
|
|
400
430
|
.optional(),
|
|
401
431
|
model: optionalText,
|
|
402
432
|
}).optional(),
|
|
433
|
+
/** The decision model (server/decider): a fast classifier that picks
|
|
434
|
+
* things for bots, starting with who answers a room message. `key` is
|
|
435
|
+
* write-only like every credential; `baseUrl` points at a Jev-compatible
|
|
436
|
+
* server instead of TypeSafe's (an operator setting with no UI); `jobs`
|
|
437
|
+
* switches each decision on or off. Not `decisions`: that section is the
|
|
438
|
+
* authorization log's retention. */
|
|
439
|
+
decider: z.object({
|
|
440
|
+
enabled: z.boolean().optional(),
|
|
441
|
+
provider: z.enum(["jev", "off"]).optional(),
|
|
442
|
+
key: optionalText,
|
|
443
|
+
baseUrl: z
|
|
444
|
+
.string()
|
|
445
|
+
.trim()
|
|
446
|
+
.max(2048)
|
|
447
|
+
.refine((value) => !value || /^https?:\/\//i.test(value), "the decision model address must start with http:// or https://")
|
|
448
|
+
.optional(),
|
|
449
|
+
jobs: z.object({ roomRouting: z.boolean().optional() }).optional(),
|
|
450
|
+
}).optional(),
|
|
403
451
|
/** Avatar provider credentials stay separate; choosing a router never reuses a cloud key. */
|
|
404
452
|
imageGen: z.object({
|
|
405
453
|
provider: z.enum(["openai", "xai", "custom"]).optional(),
|
|
@@ -429,10 +477,26 @@ const appConfigSchema = z.object({
|
|
|
429
477
|
compactAt: z.number().positive().max(10_000_000).optional(),
|
|
430
478
|
autoCompact: z.boolean().optional(),
|
|
431
479
|
}).optional(),
|
|
480
|
+
/** Memory upkeep timing, for bots with Memory upkeep switched on. */
|
|
481
|
+
memory: z.object({
|
|
482
|
+
/** Quiet time after a turn before its facts are captured (ms). */
|
|
483
|
+
captureQuietMs: z.number().int().min(1_000).max(24 * 60 * 60_000).optional(),
|
|
484
|
+
/** Local hour (0-23) after which the nightly tidy-up runs. */
|
|
485
|
+
tidyHour: z.number().int().min(0).max(23).optional(),
|
|
486
|
+
}).strict().optional(),
|
|
432
487
|
threads: threadsConfigSchema.optional(),
|
|
433
488
|
/** The authorization decision log (server/decision-log.ts): days of month
|
|
434
489
|
* files kept, at least; OMB_DECISION_RETENTION_DAYS wins when set. */
|
|
435
490
|
decisions: z.object({ retentionDays: z.number().int().min(1).max(3650).optional() }).strict().optional(),
|
|
491
|
+
/** #1655 cloud-overflow settings. perSecondCostUsd is the operator's own
|
|
492
|
+
* verified rate: with no price configured the feature stays inert rather
|
|
493
|
+
* than show an invented one. allowlistedThreads carries standing consent
|
|
494
|
+
* for exact thread ids. */
|
|
495
|
+
cloudOverflow: z.object({
|
|
496
|
+
perSecondCostUsd: z.number().positive().max(10).optional(),
|
|
497
|
+
idleStopMs: z.number().int().positive().max(24 * 60 * 60_000).optional(),
|
|
498
|
+
allowlistedThreads: z.array(z.string().min(1)).max(1000).optional(),
|
|
499
|
+
}).strict().optional(),
|
|
436
500
|
localVm: localVmConfigSchema.optional(),
|
|
437
501
|
features: featureConfigSchema.optional(),
|
|
438
502
|
onboarding: onboardingConfigSchema.optional(),
|
|
@@ -541,8 +605,9 @@ export function roomTurnTimeoutMinutes(cfg) {
|
|
|
541
605
|
return cfg.rooms?.turnTimeoutMinutes ?? DEFAULT_ROOM_TURN_TIMEOUT_MINUTES;
|
|
542
606
|
}
|
|
543
607
|
/** Room handoff tree budgets in milliseconds. The tree lifetime pauses
|
|
544
|
-
* while a node is actively executing; the hard cap is
|
|
545
|
-
*
|
|
608
|
+
* while a node is actively executing; the hard cap is the maximum stall
|
|
609
|
+
* window, measured wall-clock from the tree's last durable progress. Read
|
|
610
|
+
* when the server starts. */
|
|
546
611
|
export function roomHandoffLimits(cfg) {
|
|
547
612
|
return {
|
|
548
613
|
lifetimeMs: (cfg.rooms?.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) * 60_000,
|
|
@@ -578,13 +643,34 @@ export function localVmMaxInstances(cfg) {
|
|
|
578
643
|
export function skillAuthoringEnabled(cfg) {
|
|
579
644
|
return cfg.features?.skillAuthoring !== false;
|
|
580
645
|
}
|
|
646
|
+
/** Automatic recall is read-only, so it is on unless switched off. */
|
|
647
|
+
export function autoRecallEnabled(cfg) {
|
|
648
|
+
return cfg.features?.autoRecall !== false;
|
|
649
|
+
}
|
|
650
|
+
export const DEFAULT_CAPTURE_QUIET_MS = 2 * 60_000;
|
|
651
|
+
export const DEFAULT_TIDY_HOUR = 3;
|
|
652
|
+
export function captureQuietMs(cfg) {
|
|
653
|
+
return cfg.memory?.captureQuietMs ?? DEFAULT_CAPTURE_QUIET_MS;
|
|
654
|
+
}
|
|
655
|
+
export function tidyHour(cfg) {
|
|
656
|
+
return cfg.memory?.tidyHour ?? DEFAULT_TIDY_HOUR;
|
|
657
|
+
}
|
|
581
658
|
export function showToolCallsEnabled(cfg) {
|
|
582
659
|
return cfg.features?.showToolCalls === true;
|
|
583
660
|
}
|
|
661
|
+
/** A routine's turns are posted in the conversation that receives its card.
|
|
662
|
+
* Off by default, so scheduled work stays in a hidden thread. */
|
|
663
|
+
export function routinesInConversationEnabled(cfg) {
|
|
664
|
+
return cfg.features?.routinesInConversation === true;
|
|
665
|
+
}
|
|
584
666
|
/** Workspace-level gate for the experimental built-in browser. A bot's own
|
|
585
|
-
* switch sits under it, so either can withhold the browser.
|
|
586
|
-
|
|
587
|
-
|
|
667
|
+
* switch sits under it, so either can withhold the browser. Off until the
|
|
668
|
+
* desktop's first-run welcome turns it on. A Cloud home (cloud-home.ts)
|
|
669
|
+
* skips that welcome, ships the browser in its image and has no other screen
|
|
670
|
+
* of its own, so there it is on unless the person switched it off. */
|
|
671
|
+
export function builtInBrowserEnabled(cfg, env = process.env) {
|
|
672
|
+
const chosen = cfg.features?.browser;
|
|
673
|
+
return chosen === undefined ? cloudHomeConfigured(env) : chosen === true;
|
|
588
674
|
}
|
|
589
675
|
/** Opt-in computer sharing: the routes, the agent tools, the advertised
|
|
590
676
|
* capability and the desktop connector. Off unless an explicit `true` turns
|
|
@@ -614,6 +700,43 @@ export function claudeUserMcpEnabled(cfg) {
|
|
|
614
700
|
export function llmThreadTitlesEnabled(cfg) {
|
|
615
701
|
return cfg.features?.llmThreadTitles === true;
|
|
616
702
|
}
|
|
703
|
+
/** Idle release for computer claims (#1653): a whole-turn desktop hold
|
|
704
|
+
* ends after a screen-quiet window (default 90 seconds, screen-poller
|
|
705
|
+
* frames excluded) instead of at turn settle, and the previous holder
|
|
706
|
+
* re-claims directly inside a reclaim window (default 10 minutes) while
|
|
707
|
+
* yielding to a seat another turn already holds. Off unless an explicit
|
|
708
|
+
* `true` — bake it as a maintainer-only flag first, exactly like
|
|
709
|
+
* sharedComputers: enable by hand in ~/.openmausbot/config.json
|
|
710
|
+
* (`{"features": {"computerClaimIdleRelease": true}}`) and restart. */
|
|
711
|
+
export function computerClaimIdleReleaseEnabled(cfg) {
|
|
712
|
+
return cfg.features?.computerClaimIdleRelease === true;
|
|
713
|
+
}
|
|
714
|
+
/** Consented cloud overflow for local computer waits (#1655): while a turn
|
|
715
|
+
* waits for a local desktop, it may offer overflow to a per-second-billed
|
|
716
|
+
* cloud seat — never silently: the card names the cost, and only an
|
|
717
|
+
* explicit per-conversation consent or a configured allowlist thread may
|
|
718
|
+
* start the machine, which then hard-stops after an idle window. Off
|
|
719
|
+
* unless an explicit `true`, like computerClaimIdleRelease: enable by
|
|
720
|
+
* hand in ~/.openmausbot/config.json
|
|
721
|
+
* (`{"features": {"cloudOverflow": true}, "cloudOverflow": {"perSecondCostUsd": 0.0004}}`)
|
|
722
|
+
* and restart. */
|
|
723
|
+
export function cloudOverflowEnabled(cfg) {
|
|
724
|
+
return cfg.features?.cloudOverflow === true;
|
|
725
|
+
}
|
|
726
|
+
/** The per-second rate shown on the card, or null when the operator has
|
|
727
|
+
* not set one: the offer fails closed rather than guess a price. */
|
|
728
|
+
export function cloudOverflowPerSecondCostUsd(cfg) {
|
|
729
|
+
const cost = cfg.cloudOverflow?.perSecondCostUsd;
|
|
730
|
+
return typeof cost === "number" && Number.isFinite(cost) && cost > 0 ? cost : null;
|
|
731
|
+
}
|
|
732
|
+
/** How long a cloud seat may stay screen-quiet before it is stopped. */
|
|
733
|
+
export function cloudOverflowIdleStopMs(cfg) {
|
|
734
|
+
return cfg.cloudOverflow?.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
|
|
735
|
+
}
|
|
736
|
+
/** Threads with standing consent, by exact thread id. */
|
|
737
|
+
export function cloudOverflowAllowlistedThreads(cfg) {
|
|
738
|
+
return new Set(cfg.cloudOverflow?.allowlistedThreads ?? []);
|
|
739
|
+
}
|
|
617
740
|
/** Config sections no provider driver reads. A write that touches only
|
|
618
741
|
* these must not rebuild the fleet: rebuilding disposes every engine child
|
|
619
742
|
* and reloads it, seconds of work that would also interrupt in-flight
|
|
@@ -623,14 +746,18 @@ export const FLEET_NEUTRAL_KEYS = new Set([
|
|
|
623
746
|
"profile",
|
|
624
747
|
"language",
|
|
625
748
|
"tts",
|
|
749
|
+
// no engine reads it: the harness asks it before a turn starts
|
|
750
|
+
"decider",
|
|
626
751
|
"imageGen",
|
|
627
752
|
"vps",
|
|
628
753
|
"rooms",
|
|
629
754
|
"threads",
|
|
630
755
|
"automaticRecovery",
|
|
631
756
|
"context",
|
|
757
|
+
"memory",
|
|
632
758
|
"localVm",
|
|
633
759
|
"features",
|
|
760
|
+
"cloudOverflow",
|
|
634
761
|
"browserProfiles",
|
|
635
762
|
"onboarding",
|
|
636
763
|
]);
|
|
@@ -752,6 +879,7 @@ export function loadConfig() {
|
|
|
752
879
|
cfg.composio = { ...cfg.composio };
|
|
753
880
|
if (process.env.COMPOSIO_API_KEY !== undefined)
|
|
754
881
|
cfg.composio.apiKey = process.env.COMPOSIO_API_KEY;
|
|
882
|
+
// BOX_TOKEN keeps its historical name; the provider is Boat.
|
|
755
883
|
cfg.box = { ...cfg.box };
|
|
756
884
|
if (process.env.BOX_TOKEN !== undefined)
|
|
757
885
|
cfg.box.token = process.env.BOX_TOKEN;
|
|
@@ -761,8 +889,16 @@ export function loadConfig() {
|
|
|
761
889
|
cfg.tts = { ...cfg.tts };
|
|
762
890
|
if (process.env.OMB_TTS_KEY !== undefined)
|
|
763
891
|
cfg.tts.key = process.env.OMB_TTS_KEY;
|
|
892
|
+
// A preset ElevenLabs voice (Cloud Pro sets one) is only a default: a voice or
|
|
893
|
+
// another speech provider the person picked in Settings always wins.
|
|
894
|
+
const presetVoice = process.env.OMB_TTS_DEFAULT_VOICE?.trim();
|
|
895
|
+
if (presetVoice && !cfg.tts.voice?.trim() && (cfg.tts.provider ?? "elevenlabs") === "elevenlabs")
|
|
896
|
+
cfg.tts.voice = presetVoice;
|
|
764
897
|
if (process.env.OMB_FISH_AUDIO_API_KEY !== undefined)
|
|
765
898
|
cfg.tts.fishKey = process.env.OMB_FISH_AUDIO_API_KEY;
|
|
899
|
+
cfg.decider = { ...cfg.decider };
|
|
900
|
+
if (process.env.OMB_JEV_API_KEY !== undefined)
|
|
901
|
+
cfg.decider.key = process.env.OMB_JEV_API_KEY;
|
|
766
902
|
cfg.imageGen = { ...cfg.imageGen };
|
|
767
903
|
if (process.env.OMB_OPENAI_IMAGE_KEY !== undefined)
|
|
768
904
|
cfg.imageGen.key = process.env.OMB_OPENAI_IMAGE_KEY;
|
|
@@ -798,6 +934,7 @@ export function syncCredentialEnv(patch) {
|
|
|
798
934
|
[patch.opencodeGo?.apiKey, "OPENCODE_API_KEY"],
|
|
799
935
|
[patch.tts?.key, "OMB_TTS_KEY"],
|
|
800
936
|
[patch.tts?.fishKey, "OMB_FISH_AUDIO_API_KEY"],
|
|
937
|
+
[patch.decider?.key, "OMB_JEV_API_KEY"],
|
|
801
938
|
[patch.imageGen?.key, "OMB_OPENAI_IMAGE_KEY"],
|
|
802
939
|
[patch.imageGen?.customApiKey, "OMB_CUSTOM_IMAGE_KEY"],
|
|
803
940
|
];
|
|
@@ -844,10 +981,17 @@ export const WORKSPACE_CREDENTIAL_ENV = [
|
|
|
844
981
|
"OPENCODE_API_KEY",
|
|
845
982
|
"OMB_TTS_KEY",
|
|
846
983
|
"OMB_FISH_AUDIO_API_KEY",
|
|
984
|
+
"OMB_JEV_API_KEY",
|
|
847
985
|
"OMB_OPENAI_IMAGE_KEY",
|
|
848
986
|
"OMB_CUSTOM_IMAGE_KEY",
|
|
849
987
|
"COMPOSIO_API_KEY",
|
|
850
988
|
"OMB_COMPOSIO_BROKER_TOKEN",
|
|
989
|
+
// Cloud Pro's included Boat, voice and decision relay tokens
|
|
990
|
+
// (included-services.ts), used only in-process by the Boat, voice and
|
|
991
|
+
// decider modules.
|
|
992
|
+
"OMB_CLOUD_BOAT_TOKEN",
|
|
993
|
+
"OMB_CLOUD_VOICE_TOKEN",
|
|
994
|
+
"OMB_CLOUD_DECIDER_TOKEN",
|
|
851
995
|
// Harness-private filesystem hints are not credentials themselves, but
|
|
852
996
|
// exposing them to a shell-capable agent points straight at app-owned
|
|
853
997
|
// state. The built-in browser master is delivered privately in memory.
|
|
@@ -929,7 +1073,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
929
1073
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
930
1074
|
if (storedProfiles.success)
|
|
931
1075
|
disk.browserProfiles = storedProfiles.data;
|
|
932
|
-
for (const key of ["xai", "anthropic", "mistral", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "threads", "context", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1076
|
+
for (const key of ["xai", "anthropic", "mistral", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
933
1077
|
const section = checkedPatch[key];
|
|
934
1078
|
if (!section)
|
|
935
1079
|
continue;
|
|
@@ -1138,6 +1282,9 @@ function injectedEnvironment(cfg, driver) {
|
|
|
1138
1282
|
environment.set("OPENAI_COMPAT_API_KEY", cfg.openaiCompat.key);
|
|
1139
1283
|
if (driver === "openai-compat" && cfg.openaiCompat?.url)
|
|
1140
1284
|
environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
|
|
1285
|
+
// driverKind "boxAgent" and env BOX_TOKEN keep their historical names.
|
|
1286
|
+
// Only the person's own token: without one the driver itself falls back
|
|
1287
|
+
// to Cloud Pro's included token, which never enters an environment map.
|
|
1141
1288
|
if (driver === "boxAgent" && cfg.box?.token)
|
|
1142
1289
|
environment.set("BOX_TOKEN", cfg.box.token);
|
|
1143
1290
|
if (driver === "opencodeGo" && cfg.opencodeGo?.apiKey)
|
|
@@ -69,6 +69,19 @@ export function perBotLocalVmTarget(botId) {
|
|
|
69
69
|
label: digest,
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
+
/** A pool-mode seat (issue #1654): one of N desktops shared by every
|
|
73
|
+
* conversation, addressed by seat index. Identities stay disjoint from the
|
|
74
|
+
* shared singleton and from per-bot digests, so discovery, labels, and lease
|
|
75
|
+
* lanes never confuse the three modes. */
|
|
76
|
+
export function poolLocalVmTarget(seat) {
|
|
77
|
+
return {
|
|
78
|
+
key: `pool:${seat}`,
|
|
79
|
+
containerName: `${CONTAINER}-p${seat}`,
|
|
80
|
+
workspaceDir: join(DATA_DIR, "vm-homes", `pool-${seat}`),
|
|
81
|
+
viewerPort: null,
|
|
82
|
+
label: `pool-${seat}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
72
85
|
/** Only provisioning creates this durable directory; idle removal keeps it. */
|
|
73
86
|
export function localVmWorkspaceExists(target) {
|
|
74
87
|
try {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The proxy-side half of computer control. The harness keeps the record of
|
|
2
2
|
// who is driving (server/computer-control.ts); the per-turn computer
|
|
3
3
|
// processes consult it through this client before acting, because the
|
|
4
|
-
// action paths themselves never traverse the harness — a
|
|
5
|
-
// straight to the
|
|
4
|
+
// action paths themselves never traverse the harness — a boat click goes
|
|
5
|
+
// straight to the boat's REST API, and a Local VM / VPS click rides a
|
|
6
6
|
// transparent stdio bridge into Cua Driver.
|
|
7
7
|
//
|
|
8
8
|
// Failure posture: CLOSED once configured. A missing/expired turn capability
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { deciderCredential } from "../included-services.js";
|
|
2
|
+
import { jevBackend } from "./jev.js";
|
|
3
|
+
import { appendDeciderLog, stateHash } from "./log.js";
|
|
4
|
+
import { KEY_CHECK_QUESTION, KEY_CHECK_STATE, relayAccepts, relaySeam } from "./relay.js";
|
|
5
|
+
export { DECIDER_JOBS } from "./types.js";
|
|
6
|
+
/** Used when a caller passes no timeout. Room routing passes its own. */
|
|
7
|
+
export const DEFAULT_DECIDER_TIMEOUT_MS = 1_500;
|
|
8
|
+
/** The Settings key check is one tiny call a person is waiting on. */
|
|
9
|
+
export const KEY_CHECK_TIMEOUT_MS = 10_000;
|
|
10
|
+
const MIN_TIMEOUT_MS = 50;
|
|
11
|
+
const MAX_TIMEOUT_MS = 30_000;
|
|
12
|
+
/** A request the switches never let through. */
|
|
13
|
+
const offBackend = {
|
|
14
|
+
id: "off",
|
|
15
|
+
decide: async () => ({ ok: false, reason: "disabled" }),
|
|
16
|
+
};
|
|
17
|
+
const BACKENDS = { jev: jevBackend };
|
|
18
|
+
export function deciderProvider(cfg) {
|
|
19
|
+
const provider = cfg.decider?.provider ?? "jev";
|
|
20
|
+
return provider in BACKENDS ? provider : "off";
|
|
21
|
+
}
|
|
22
|
+
/** The key a call uses and the one base URL it goes to: the person's own
|
|
23
|
+
* (a draft being tested, else the config value or the desktop's encrypted
|
|
24
|
+
* store handed over as env) with `decider.baseUrl` or Jev's own, else Cloud
|
|
25
|
+
* Pro's included token with the Admin's relay, but only where the relay
|
|
26
|
+
* accepts that seam: any other job has only an own key. Resolved on every
|
|
27
|
+
* call. */
|
|
28
|
+
function deciderAccount(cfg, seam, draft) {
|
|
29
|
+
const account = deciderCredential(draft?.trim() || cfg.decider?.key, cfg.decider?.baseUrl);
|
|
30
|
+
return account?.included && seam && !relaySeam(seam) ? null : account;
|
|
31
|
+
}
|
|
32
|
+
/** A key is on file, or Cloud Pro includes decisions here. */
|
|
33
|
+
export function deciderConfigured(cfg) {
|
|
34
|
+
return Boolean(deciderAccount(cfg));
|
|
35
|
+
}
|
|
36
|
+
/** Cloud Pro includes decisions on this machine, whatever key is saved:
|
|
37
|
+
* what clearing an own key falls back to. */
|
|
38
|
+
export function deciderIncludedHere() {
|
|
39
|
+
return deciderCredential(undefined, undefined)?.included === true;
|
|
40
|
+
}
|
|
41
|
+
/** The master switch as it takes effect: off while there is no key. With an
|
|
42
|
+
* own key it is on once switched on (saving the key does that); with Cloud
|
|
43
|
+
* Pro's included decisions it is on until someone switches it off. */
|
|
44
|
+
export function deciderEnabled(cfg) {
|
|
45
|
+
const account = deciderAccount(cfg);
|
|
46
|
+
if (!account || deciderProvider(cfg) === "off")
|
|
47
|
+
return false;
|
|
48
|
+
return account.included ? cfg.decider?.enabled !== false : cfg.decider?.enabled === true;
|
|
49
|
+
}
|
|
50
|
+
/** One job's own switch. Absent means on: turning the decider on turns on
|
|
51
|
+
* what it decides unless a job was switched off by hand. */
|
|
52
|
+
export function deciderJobOn(cfg, job) {
|
|
53
|
+
return cfg.decider?.jobs?.[job] !== false;
|
|
54
|
+
}
|
|
55
|
+
/** Whether a job would be asked right now. Callers check this before they
|
|
56
|
+
* spend any effort building a request. */
|
|
57
|
+
export function deciderReady(cfg, job) {
|
|
58
|
+
return deciderEnabled(cfg) && deciderJobOn(cfg, job) && Boolean(deciderAccount(cfg, job));
|
|
59
|
+
}
|
|
60
|
+
/** What Settings needs. Never the key, and not the base URL either: that is
|
|
61
|
+
* an operator setting with no UI. `included`: the decisions are Cloud Pro's,
|
|
62
|
+
* not a saved key. */
|
|
63
|
+
export function describeDecider(cfg) {
|
|
64
|
+
const account = deciderAccount(cfg);
|
|
65
|
+
return {
|
|
66
|
+
provider: "jev",
|
|
67
|
+
configured: Boolean(account),
|
|
68
|
+
...(account?.included ? { included: true } : {}),
|
|
69
|
+
enabled: deciderEnabled(cfg),
|
|
70
|
+
jobs: { roomRouting: deciderJobOn(cfg, "roomRouting") },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** The config write a Settings save becomes.
|
|
74
|
+
*
|
|
75
|
+
* Owner rule: the switch is off while no key is saved, and saving a key is
|
|
76
|
+
* the "turn it on" — it switches the decider and its room job on, with no
|
|
77
|
+
* extra step. Clearing the key switches it off, unless Cloud Pro includes
|
|
78
|
+
* decisions here (`included`): then it falls back to them as it stands.
|
|
79
|
+
* Explicit values in the same patch win. Jobs merge per job, because
|
|
80
|
+
* saveConfig replaces a section's nested objects whole. Returns an error for
|
|
81
|
+
* a switch-on with no key and nothing included. */
|
|
82
|
+
export function deciderSavePatch(patch, current, included = false) {
|
|
83
|
+
const next = { ...patch };
|
|
84
|
+
const savingKey = typeof patch.key === "string" && Boolean(patch.key.trim());
|
|
85
|
+
const clearingKey = typeof patch.key === "string" && !patch.key.trim();
|
|
86
|
+
if (typeof patch.key === "string")
|
|
87
|
+
next.key = patch.key.trim();
|
|
88
|
+
if (savingKey) {
|
|
89
|
+
next.enabled = patch.enabled ?? true;
|
|
90
|
+
next.jobs = { ...current?.jobs, roomRouting: true, ...patch.jobs };
|
|
91
|
+
}
|
|
92
|
+
else if (clearingKey) {
|
|
93
|
+
if (!included)
|
|
94
|
+
next.enabled = false;
|
|
95
|
+
}
|
|
96
|
+
else if (patch.jobs) {
|
|
97
|
+
next.jobs = { ...current?.jobs, ...patch.jobs };
|
|
98
|
+
}
|
|
99
|
+
if (next.enabled === true && !savingKey && !current?.key?.trim() && !included) {
|
|
100
|
+
return { ok: false, error: "Save a Jev API key before turning on fast decisions." };
|
|
101
|
+
}
|
|
102
|
+
return { ok: true, patch: next };
|
|
103
|
+
}
|
|
104
|
+
export function createDecider(deps) {
|
|
105
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
106
|
+
const now = deps.now ?? (() => performance.now());
|
|
107
|
+
function gate(cfg, seam) {
|
|
108
|
+
if (deciderProvider(cfg) === "off")
|
|
109
|
+
return "disabled";
|
|
110
|
+
if (seam === "keyCheck")
|
|
111
|
+
return null;
|
|
112
|
+
if (!deciderConfigured(cfg))
|
|
113
|
+
return cfg.decider?.enabled === true ? "no_key" : "disabled";
|
|
114
|
+
if (!deciderEnabled(cfg))
|
|
115
|
+
return "disabled";
|
|
116
|
+
if (!deciderJobOn(cfg, seam))
|
|
117
|
+
return "job_off";
|
|
118
|
+
// Only the included token, and the relay does not take this job.
|
|
119
|
+
if (!deciderAccount(cfg, seam))
|
|
120
|
+
return "no_key";
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
async function call(seam, backend, request, options) {
|
|
124
|
+
const provider = backend.id === "off" ? undefined : backend.id;
|
|
125
|
+
if (options.signal?.aborted)
|
|
126
|
+
return { ok: false, ...(provider ? { provider } : {}), reason: "cancelled" };
|
|
127
|
+
const timeoutMs = Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, options.timeoutMs ?? DEFAULT_DECIDER_TIMEOUT_MS));
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const onAbort = () => controller.abort();
|
|
130
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
131
|
+
let timer;
|
|
132
|
+
// The race, not only the abort, bounds the call: a body that stalls
|
|
133
|
+
// after its headers must not hold a room turn past the budget.
|
|
134
|
+
const timedOut = new Promise((resolve) => {
|
|
135
|
+
timer = setTimeout(() => {
|
|
136
|
+
controller.abort();
|
|
137
|
+
resolve({ ok: false, reason: "timeout" });
|
|
138
|
+
}, timeoutMs);
|
|
139
|
+
});
|
|
140
|
+
const started = now();
|
|
141
|
+
let outcome;
|
|
142
|
+
try {
|
|
143
|
+
const decided = Promise.resolve()
|
|
144
|
+
.then(() => backend.decide({ ...request, signal: controller.signal, fetch: fetchImpl }))
|
|
145
|
+
.catch(() => ({ ok: false, reason: "malformed" }));
|
|
146
|
+
outcome = await Promise.race([decided, timedOut]);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
151
|
+
}
|
|
152
|
+
const latencyMs = Math.max(0, Math.round(now() - started));
|
|
153
|
+
if (!outcome.ok && outcome.reason !== "timeout" && options.signal?.aborted)
|
|
154
|
+
outcome = { ok: false, reason: "cancelled" };
|
|
155
|
+
if (provider && deps.dataDir) {
|
|
156
|
+
const first = outcome.ok ? Object.values(outcome.answers).find((answer) => answer.type === "choice") : undefined;
|
|
157
|
+
appendDeciderLog(deps.dataDir, {
|
|
158
|
+
at: new Date().toISOString(),
|
|
159
|
+
seam,
|
|
160
|
+
provider,
|
|
161
|
+
ok: outcome.ok,
|
|
162
|
+
...(!outcome.ok ? { reason: outcome.reason, ...(outcome.status ? { status: outcome.status } : {}) } : {}),
|
|
163
|
+
...(first ? { choice: first.choice } : {}),
|
|
164
|
+
pTop: first?.pTop ?? null,
|
|
165
|
+
margin: first?.margin ?? null,
|
|
166
|
+
latencyMs,
|
|
167
|
+
...(outcome.ok && outcome.inputTokens !== undefined ? { inputTokens: outcome.inputTokens } : {}),
|
|
168
|
+
stateHash: stateHash(request.state),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (!outcome.ok)
|
|
172
|
+
return { ok: false, ...(provider ? { provider } : {}), reason: outcome.reason, ...(outcome.status ? { status: outcome.status } : {}), latencyMs };
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
provider: provider,
|
|
176
|
+
answers: outcome.answers,
|
|
177
|
+
latencyMs,
|
|
178
|
+
...(outcome.inputTokens !== undefined ? { inputTokens: outcome.inputTokens } : {}),
|
|
179
|
+
...(outcome.model ? { model: outcome.model } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function ask(seam, state, questions, options = {}) {
|
|
183
|
+
try {
|
|
184
|
+
const cfg = deps.config();
|
|
185
|
+
const refused = gate(cfg, seam);
|
|
186
|
+
if (refused)
|
|
187
|
+
return { ok: false, reason: refused };
|
|
188
|
+
const provider = deciderProvider(cfg);
|
|
189
|
+
const backend = provider === "off" ? offBackend : BACKENDS[provider];
|
|
190
|
+
const account = deciderAccount(cfg, seam);
|
|
191
|
+
// Through the included token, only the exact requests the relay takes.
|
|
192
|
+
if (account.included && !relayAccepts(seam, state, questions))
|
|
193
|
+
return { ok: false, reason: "misconfigured" };
|
|
194
|
+
return await call(seam, backend, { key: account.token, baseUrl: account.api, state, questions }, options);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return { ok: false, reason: "malformed" };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const single = (result) => result.ok ? { ...result, answers: result.answers.answer } : result;
|
|
201
|
+
return {
|
|
202
|
+
ask,
|
|
203
|
+
async choose(seam, state, question, options) {
|
|
204
|
+
const asked = { answer: { type: "choice", instructions: question.instructions, options: question.options } };
|
|
205
|
+
return single(await ask(seam, state, asked, options));
|
|
206
|
+
},
|
|
207
|
+
async score(seam, state, question, options) {
|
|
208
|
+
return single(await ask(seam, state, { answer: { type: "score", instructions: question.instructions, levels: question.levels } }, options));
|
|
209
|
+
},
|
|
210
|
+
async yesNo(seam, state, instructions, options) {
|
|
211
|
+
return single(await ask(seam, state, { answer: { type: "yesno", instructions } }, options));
|
|
212
|
+
},
|
|
213
|
+
async testKey(input = {}, options = {}) {
|
|
214
|
+
try {
|
|
215
|
+
const cfg = deps.config();
|
|
216
|
+
// A draft or saved own key goes to Jev (or decider.baseUrl); with
|
|
217
|
+
// neither, Cloud Pro's included token goes to its relay.
|
|
218
|
+
const account = deciderAccount(cfg, "keyCheck", input.key);
|
|
219
|
+
if (!account)
|
|
220
|
+
return { ok: false, reason: "no_key" };
|
|
221
|
+
const provider = deciderProvider(cfg);
|
|
222
|
+
const backend = provider === "off" ? offBackend : BACKENDS[provider];
|
|
223
|
+
const questions = { answer: { type: "yesno", instructions: KEY_CHECK_QUESTION } };
|
|
224
|
+
if (account.included && !relayAccepts("keyCheck", KEY_CHECK_STATE, questions))
|
|
225
|
+
return { ok: false, reason: "misconfigured" };
|
|
226
|
+
return single(await call("keyCheck", backend, {
|
|
227
|
+
key: account.token,
|
|
228
|
+
baseUrl: account.api,
|
|
229
|
+
state: KEY_CHECK_STATE,
|
|
230
|
+
questions,
|
|
231
|
+
}, { timeoutMs: KEY_CHECK_TIMEOUT_MS, ...options }));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return { ok: false, reason: "malformed" };
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|