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.
Files changed (168) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
  2. package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
  6. package/dist/assets/channel-DrfGMu7X.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
  33. package/dist/assets/index-BlnCEcU6.js +588 -0
  34. package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
  35. package/dist/assets/index-DVxjOvID.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
  40. package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +14810 -0
  61. package/dist-server/container-mcp.js +136 -36
  62. package/dist-server/drivers/agents-proxy.js +31 -10
  63. package/dist-server/index.js +48251 -44124
  64. package/dist-server/openmausbot.js +628 -292
  65. package/dist-server/pair-cli.js +628 -292
  66. package/dist-server/permission-proxy.js +1 -1
  67. package/dist-server/server/admission.js +46 -0
  68. package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
  69. package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
  70. package/dist-server/server/{box.js → boat.js} +381 -334
  71. package/dist-server/server/bot-overview.js +7 -4
  72. package/dist-server/server/bot-profile.js +7 -1
  73. package/dist-server/server/channel-queue.js +58 -18
  74. package/dist-server/server/checkpoints.js +98 -52
  75. package/dist-server/server/claim-idle.js +33 -0
  76. package/dist-server/server/cloud-backend.js +5 -5
  77. package/dist-server/server/cloud-home-start.js +121 -0
  78. package/dist-server/server/cloud-home.js +241 -0
  79. package/dist-server/server/cloud-lending.js +103 -0
  80. package/dist-server/server/cloud-move-http.js +268 -0
  81. package/dist-server/server/cloud-move.js +418 -0
  82. package/dist-server/server/cloud-overflow.js +167 -0
  83. package/dist-server/server/composio.js +5 -0
  84. package/dist-server/server/computer-observation.js +2 -2
  85. package/dist-server/server/config.js +156 -9
  86. package/dist-server/server/container-computer.js +13 -0
  87. package/dist-server/server/control-client.js +2 -2
  88. package/dist-server/server/decider/index.js +238 -0
  89. package/dist-server/server/decider/jev.js +183 -0
  90. package/dist-server/server/decider/log.js +62 -0
  91. package/dist-server/server/decider/relay.js +52 -0
  92. package/dist-server/server/decider/room-routing.js +88 -0
  93. package/dist-server/server/decider/types.js +5 -0
  94. package/dist-server/server/delegations.js +11 -3
  95. package/dist-server/server/drivers/acp/core.js +5 -4
  96. package/dist-server/server/drivers/agents-call.js +5 -1
  97. package/dist-server/server/drivers/agents-catalog.js +25 -3
  98. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
  99. package/dist-server/server/drivers/builtIn.js +2 -2
  100. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  101. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  102. package/dist-server/server/drivers/claude.js +178 -13
  103. package/dist-server/server/drivers/native.js +1 -1
  104. package/dist-server/server/drivers/openai-chat.js +15 -4
  105. package/dist-server/server/drivers/pi.js +3 -3
  106. package/dist-server/server/group-goal-run.js +6 -4
  107. package/dist-server/server/harness-capabilities.js +2 -0
  108. package/dist-server/server/hosted-models.js +6 -1
  109. package/dist-server/server/included-services.js +70 -0
  110. package/dist-server/server/index.js +1462 -576
  111. package/dist-server/server/local-vm-inventory.js +11 -1
  112. package/dist-server/server/local-vm-seat-pool.js +141 -0
  113. package/dist-server/server/managed-policy.js +4 -1
  114. package/dist-server/server/memory-capture.js +194 -0
  115. package/dist-server/server/memory-entries.js +76 -0
  116. package/dist-server/server/memory-organize.js +102 -0
  117. package/dist-server/server/memory-tidy.js +144 -0
  118. package/dist-server/server/memory-topics.js +169 -0
  119. package/dist-server/server/memory-upkeep.js +452 -0
  120. package/dist-server/server/message-db.js +49 -5
  121. package/dist-server/server/model-context-window.js +4 -4
  122. package/dist-server/server/package-export.js +17 -10
  123. package/dist-server/server/package-import.js +6 -3
  124. package/dist-server/server/package-parts.js +3 -1
  125. package/dist-server/server/permission-proxy.js +3 -3
  126. package/dist-server/server/profile-learned.js +98 -0
  127. package/dist-server/server/profile-versions.js +45 -11
  128. package/dist-server/server/recall.js +172 -0
  129. package/dist-server/server/redact.js +1 -1
  130. package/dist-server/server/request-auth.js +2 -0
  131. package/dist-server/server/room-cwd.js +1 -1
  132. package/dist-server/server/room-handoffs.js +61 -6
  133. package/dist-server/server/routes/bot-memory.js +215 -0
  134. package/dist-server/server/routes/decider.js +19 -0
  135. package/dist-server/server/routine-requests.js +27 -16
  136. package/dist-server/server/routines.js +10 -6
  137. package/dist-server/server/shared-computers.js +116 -30
  138. package/dist-server/server/steer-queue.js +41 -10
  139. package/dist-server/server/store.js +42 -3
  140. package/dist-server/server/surface.js +14 -1
  141. package/dist-server/server/system-prompt.js +16 -5
  142. package/dist-server/server/team-backup.js +6 -2
  143. package/dist-server/server/team-computers.js +2 -2
  144. package/dist-server/server/team-manifest.js +11 -3
  145. package/dist-server/server/tightening-requests.js +34 -4
  146. package/dist-server/server/tool-summary.js +2 -2
  147. package/dist-server/server/tts/elevenlabs.js +12 -7
  148. package/dist-server/server/tts/index.js +17 -10
  149. package/dist-server/server/turn-resources.js +87 -11
  150. package/dist-server/server/vps-computer.js +1 -1
  151. package/dist-server/server/workspace-backup-policy.js +22 -3
  152. package/dist-server/server/workspace-backup.js +25 -8
  153. package/dist-server/server/workspace.js +57 -22
  154. package/dist-server/shared/ask-question.js +7 -5
  155. package/dist-server/shared/bot-avatar.js +19 -0
  156. package/dist-server/shared/cloud-home.js +4 -0
  157. package/dist-server/shared/credential-request.js +3 -3
  158. package/dist-server/shared/package-format.js +30 -4
  159. package/dist-server/shared/team-backup.js +20 -3
  160. package/dist-server/shared/tool-surface.js +1 -1
  161. package/dist-server/src/lib/composer-attachments.js +9 -0
  162. package/dist-server/vps-container-mcp.js +136 -36
  163. package/dist-server/workspace-backup.worker.js +165 -35
  164. package/package.json +1 -1
  165. package/dist/assets/channel-DqY_gMLg.js +0 -1
  166. package/dist/assets/index-BIMdIbaw.css +0 -1
  167. package/dist/assets/index-BVPdbq2r.js +0 -583
  168. 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 wall-clock and bounds trees that never stop executing. */
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
- * snippet in place — see llmThreadTitlesEnabled. */
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 wall-clock and bounds
545
- * trees that never stop. Read when the server starts. */
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
- export function builtInBrowserEnabled(cfg) {
587
- return cfg.features?.browser === true;
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 box click goes
5
- // straight to the box's REST API, and a Local VM / VPS click rides a
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
+ }