openmausbot 0.1.96 → 0.1.97

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 (123) hide show
  1. package/dist/assets/{DesktopViewer-DsnA23KM.js → DesktopViewer-CPidMCtt.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
  3. package/dist/assets/{arc-Duj5Coy2.js → arc-BUWGa9UU.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-uYUsDawP.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
  7. package/dist/assets/channel-DR8eD6QW.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-D-Uglxqq.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-CKV5D2F6.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
  34. package/dist/assets/{index-DreDnjOu.js → index-Cp4S4NOL.js} +1 -1
  35. package/dist/assets/index-CpaIXRLp.js +614 -0
  36. package/dist/assets/index-Z81SMsgR.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
  41. package/dist/assets/{linear-Bl669148.js → linear-BASsfWly.js} +1 -1
  42. package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-BVidiPkk.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +1 -1
  62. package/dist-server/drivers/agents-proxy.js +5 -5
  63. package/dist-server/index.js +2379 -1596
  64. package/dist-server/openmausbot.js +857 -1012
  65. package/dist-server/pair-cli.js +857 -1012
  66. package/dist-server/permission-proxy.js +0 -4
  67. package/dist-server/server/auto-approve.js +1 -1
  68. package/dist-server/server/auto-vm-claims.js +13 -1
  69. package/dist-server/server/automatic-recovery.js +1 -2
  70. package/dist-server/server/boat.js +68 -84
  71. package/dist-server/server/bot-overview.js +1 -1
  72. package/dist-server/server/browser-engine.js +38 -1
  73. package/dist-server/server/browser-live.js +112 -25
  74. package/dist-server/server/browser-runtime.js +31 -1
  75. package/dist-server/server/checkpoints.js +275 -44
  76. package/dist-server/server/cli-setup.js +3 -3
  77. package/dist-server/server/cloud-computer-tools.js +11 -4
  78. package/dist-server/server/cloud-home-start.js +1 -1
  79. package/dist-server/server/cloud-home.js +3 -9
  80. package/dist-server/server/cloud-owner.js +1 -1
  81. package/dist-server/server/computer-engine-removal.js +70 -0
  82. package/dist-server/server/computer-selection.js +18 -0
  83. package/dist-server/server/config.js +7 -11
  84. package/dist-server/server/connector-scope-hint.js +83 -0
  85. package/dist-server/server/default-model-selection.js +20 -14
  86. package/dist-server/server/delegations.js +11 -5
  87. package/dist-server/server/delta-context.js +6 -1
  88. package/dist-server/server/drivers/acp/core.js +1 -1
  89. package/dist-server/server/drivers/acp/opencode-go.js +16 -21
  90. package/dist-server/server/drivers/agents-call.js +1 -1
  91. package/dist-server/server/drivers/agents-catalog.js +3 -3
  92. package/dist-server/server/drivers/builtIn.js +0 -2
  93. package/dist-server/server/drivers/claude.js +49 -30
  94. package/dist-server/server/drivers/codex.js +2 -2
  95. package/dist-server/server/drivers/openai-chat.js +98 -6
  96. package/dist-server/server/drivers/pi.js +184 -49
  97. package/dist-server/server/index.js +555 -279
  98. package/dist-server/server/live-call.js +11 -6
  99. package/dist-server/server/managed-policy.js +1 -1
  100. package/dist-server/server/model-catalog/presets.js +8 -0
  101. package/dist-server/server/peer-roster.js +10 -2
  102. package/dist-server/server/room-turn-end.js +15 -0
  103. package/dist-server/server/routes/live.js +3 -3
  104. package/dist-server/server/routine-requests.js +1 -1
  105. package/dist-server/server/routines.js +1 -1
  106. package/dist-server/server/screen-frame-gate.js +1 -3
  107. package/dist-server/server/store.js +125 -1
  108. package/dist-server/server/surface.js +29 -49
  109. package/dist-server/server/system-prompt.js +10 -10
  110. package/dist-server/server/turn-continuation.js +162 -0
  111. package/dist-server/server/workspace.js +1 -1
  112. package/dist-server/shared/ask-question.js +0 -70
  113. package/dist-server/shared/cloud-computer.js +4 -6
  114. package/dist-server/shared/credential-request.js +1 -1
  115. package/dist-server/shared/failed-turn.js +1 -4
  116. package/dist-server/shared/place-view.js +409 -0
  117. package/dist-server/shared/tool-scope-support.js +1 -2
  118. package/package.json +1 -1
  119. package/dist/assets/channel-BZxwC-Gw.js +0 -1
  120. package/dist/assets/index-Du7o6Dsf.js +0 -610
  121. package/dist/assets/index-KxG4bWSr.css +0 -1
  122. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
  123. package/dist-server/server/drivers/boatagent.js +0 -443
@@ -1,443 +0,0 @@
1
- import { newEventId, newId } from "../contracts.js";
2
- import { boatCredential, boatProviderApi } from "../included-services.js";
3
- import { appendNative } from "./native.js";
4
- import { assertToolScopeSupported } from "../../shared/tool-scope-support.js";
5
- import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
6
- // "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
7
- const DRIVER_KIND = "boxAgent";
8
- const MODELS = {
9
- default: "claude-fable-5",
10
- options: [
11
- { id: "claude-fable-5", label: "Claude Fable 5 · on the box" },
12
- { id: "sonnet", label: "Claude Sonnet · on the box" },
13
- { id: "gpt-5.4", label: "GPT-5.4 (Codex) · on the box" },
14
- ],
15
- };
16
- /** Any fence whose info string names the ask protocol, even when its body
17
- * does not parse: the marker for "the model tried to ask and failed". */
18
- const ASK_FENCE_ANY = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask\b/;
19
- /** The boat runs every harness boat.dev ships (claude-code, codex, pi, opencode,
20
- * prime-agent, kimi). Which one a model id belongs to comes from the public
21
- * catalog, `GET /api/provider-models` at the API root: an object keyed by
22
- * harness, each with its `models`. A bot that arrives here from another engine
23
- * carries that engine's model id, so this is what lets it keep its model. The
24
- * root is the API in use (Boat, or Cloud Pro's relay), so it is cached per root. */
25
- let catalog = null;
26
- let catalogRoot = "";
27
- async function loadCatalog(api) {
28
- const root = api.replace(/\/api\/box\/v1\/?$/, "");
29
- if (catalog && catalogRoot === root)
30
- return;
31
- catalogRoot = root;
32
- catalog = await fetch(`${root}/api/provider-models`, { signal: AbortSignal.timeout(15_000) })
33
- .then((res) => (res.ok ? res.json() : null))
34
- .catch(() => null);
35
- }
36
- const providerFor = (model) => {
37
- const slash = model.indexOf("/");
38
- if (slash > 0 && catalog?.[model.slice(0, slash)])
39
- return { provider: model.slice(0, slash), model: model.slice(slash + 1) };
40
- for (const [provider, harness] of Object.entries(catalog ?? {})) {
41
- if (harness.models?.some((m) => m?.id === model))
42
- return { provider, model };
43
- }
44
- return { provider: model.startsWith("gpt") ? "codex" : "claude-code", model };
45
- };
46
- /** Boat answers a prompt it cannot run with 409 provider_not_configured: the
47
- * Boat has no AI sign-in for the harness the model needs. Say that, and the
48
- * one thing the person can do about it here. */
49
- export const PROVIDER_NOT_CONFIGURED_MESSAGE = "The Computer engine has no AI sign-in on its cloud computer. Choose another engine in this bot's settings.";
50
- /** An OMB Cloud's included Boat account has no agent sign-ins and never
51
- * will: the operator's account must not run (or pay for) customers' models. */
52
- export const COMPUTER_ENGINE_CLOUD_UNAVAILABLE = "The Computer engine isn't available on OpenMaus Cloud — choose another engine.";
53
- /** The words of a Boat error envelope ({code, message, error:{code,
54
- * message}}), never a bare code when Boat sent a sentence. */
55
- export function boatErrorMessage(body, status) {
56
- const code = body?.code ?? body?.error?.code;
57
- if (code === "provider_not_configured")
58
- return PROVIDER_NOT_CONFIGURED_MESSAGE;
59
- const message = body?.message ?? body?.error?.message ?? (typeof body?.error === "string" ? body.error : undefined) ?? code;
60
- return typeof message === "string" && message.trim() ? message : `box HTTP ${status}`;
61
- }
62
- function decodeConfig(raw) {
63
- const o = (raw ?? {});
64
- return {
65
- pollMs: typeof o.pollMs === "number" ? o.pollMs : 2500,
66
- ...(typeof o.askTimeoutMs === "number" ? { askTimeoutMs: o.askTimeoutMs } : {}),
67
- };
68
- }
69
- export const BoatAgentDriver = {
70
- driverKind: DRIVER_KIND,
71
- metadata: { displayName: "Computer", supportsMultipleInstances: false },
72
- models: MODELS,
73
- decodeConfig,
74
- defaultConfig: () => decodeConfig({}),
75
- async create(input) {
76
- const { instanceId, config } = input;
77
- // The person's own token (instanceConfigs injects it), else Cloud Pro's
78
- // included one, with the one base URL each may be sent to. Resolved per
79
- // request, like every other Boat call.
80
- const ownToken = input.environment.BOX_TOKEN ?? process.env.BOX_TOKEN;
81
- const account = () => boatCredential(ownToken);
82
- const listeners = new Set();
83
- const active = new Map();
84
- /** One open ask per thread: the turn-held transport's pending card. The
85
- * thread is busy for exactly as long as the ask is open, so a second
86
- * block can never race the first. */
87
- const heldAsks = new Map();
88
- /** Threads whose last run ended with an unparseable omb-ask fence: the
89
- * next prompt carries the correction so the ask is never lost silently. */
90
- const malformedAsks = new Set();
91
- const emit = (event) => {
92
- for (const l of Array.from(listeners))
93
- l(event);
94
- };
95
- const base = (threadId, turnId) => ({
96
- eventId: newEventId(),
97
- provider: DRIVER_KIND,
98
- threadId,
99
- turnId,
100
- createdAt: new Date().toISOString(),
101
- });
102
- const api = async (path, opts = {}) => {
103
- const credential = account();
104
- const res = await fetch(`${credential?.api ?? boatProviderApi()}${path}`, {
105
- ...opts,
106
- headers: { authorization: `Bearer ${credential?.token ?? ""}`, "content-type": "application/json", ...opts.headers },
107
- signal: opts.signal ?? AbortSignal.timeout(30_000),
108
- });
109
- const body = await res.json().catch(() => null);
110
- if (!res.ok || body?.ok === false)
111
- throw new Error(boatErrorMessage(body, res.status));
112
- return body;
113
- };
114
- const sendTurn = async (turn) => {
115
- turn = { ...turn, toolScope: assertToolScopeSupported(DRIVER_KIND, turn.toolScope) };
116
- const { threadId } = turn;
117
- const computer = turn.integrations?.computer;
118
- const boxId = computer && (!computer.kind || computer.kind === "box") ? computer.boxId : undefined;
119
- if (!account())
120
- throw new Error('box not configured — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
121
- if (account()?.included)
122
- throw new Error(COMPUTER_ENGINE_CLOUD_UNAVAILABLE);
123
- if (!boxId) {
124
- throw new Error("this bot has no computer yet — open the Computer panel and provision one");
125
- }
126
- if (active.has(threadId))
127
- throw new Error("a turn is already running on this thread");
128
- const turnId = newId();
129
- const model = turn.model || MODELS.default;
130
- const correction = malformedAsks.delete(threadId)
131
- ? "Your previous omb-ask block was malformed or empty, so the person never saw it. Ask again with a valid fenced omb-ask JSON block, or ask in plain words."
132
- : "";
133
- const prompt = [
134
- turn.system,
135
- "You are working on the assigned cloud computer — use its desktop, Chrome, and shell within the access described above.",
136
- ombAskProtocolPrompt(),
137
- ...(correction ? [correction] : []),
138
- "",
139
- turn.text,
140
- ]
141
- .filter((s) => s !== undefined)
142
- .join("\n");
143
- const postPrompt = async (promptText) => {
144
- await loadCatalog(account()?.api ?? boatProviderApi());
145
- const started = await api(`/boxes/${boxId}/prompt`, {
146
- method: "POST",
147
- body: JSON.stringify({ ...providerFor(model), prompt: promptText }),
148
- });
149
- appendNative(threadId, { dir: "out", source: "box.prompt", msg: { model, prompt: promptText, response: started } });
150
- // real shape (2026-08): {type:"prompt.queued", promptId, promptRun:{id,…},
151
- // id:<boat id>} — never fall back to the bare id, it's the boat's
152
- return started?.promptRun?.id ?? started?.prompt?.id ?? started?.promptId ?? null;
153
- };
154
- const promptId = await postPrompt(prompt);
155
- let cancelled = false;
156
- active.set(threadId, {
157
- turnId,
158
- boxId,
159
- cancel: () => {
160
- cancelled = true;
161
- heldAsks.get(threadId)?.settle(null, "system");
162
- void api(`/boxes/${boxId}/interrupt`, { method: "POST" }).catch(() => { });
163
- },
164
- });
165
- emit({ ...base(threadId, turnId), type: "turn.started" });
166
- emit({ ...base(threadId, turnId), type: "session.started", sessionId: promptId, model });
167
- // poll events + run status until the prompt settles — and, when the
168
- // run ends on an omb-ask block, until the person answers it: the OMB
169
- // turn is the unit the whole server already understands (busy thread,
170
- // waiting-on-you, routines waiting), so it stays open across the ask
171
- // and the continuation prompt continues it rather than starting a
172
- // driver-initiated turn nobody accounts for.
173
- (async () => {
174
- // Event ids seen this TURN. The events stream is the whole
175
- // conversation, so a continuation run must not re-ingest history;
176
- // `taskId` normally filters it, but ids survive even shape drift.
177
- const seen = new Set();
178
- let lastText = "";
179
- let pendingText = "";
180
- /** Why the boat could not answer (login expired, model refused, …). */
181
- let problem = null;
182
- /** Emit unflushed deltas as assistant_text and reset pendingText.
183
- * The omb-ask block is protocol, not prose: it streamed raw (the
184
- * card is its readable form), and the settled message shows the
185
- * words around it, never the JSON. */
186
- const flushAssistantText = () => {
187
- const text = stripOmbAskBlock(pendingText);
188
- pendingText = "";
189
- if (!text.trim())
190
- return;
191
- emit({ ...base(threadId, turnId), type: "item.completed", itemType: "assistant_text", text });
192
- };
193
- /** Stream a full-text snapshot as a delta and accumulate it for flush. */
194
- const ingest = (text) => {
195
- const delta = text.startsWith(lastText) ? text.slice(lastText.length) : text;
196
- lastText = text;
197
- if (!delta)
198
- return;
199
- pendingText += delta;
200
- emit({ ...base(threadId, turnId), type: "content.delta", streamKind: "assistant_text", delta });
201
- };
202
- /** The run has settled: flush its prose, then hold the OMB turn open
203
- * on an omb-ask block (if any) until the person answers or the ask
204
- * times out. An answer chains the continuation run under this same
205
- * turn; a deny or timeout ends it. */
206
- const finishRun = async (ok, stopReason) => {
207
- flushAssistantText();
208
- // An interrupt that lands between ask text streaming and settle
209
- // finds no heldAsks entry to settle; without this check the run
210
- // would register a fresh ask for a dead turn and hold stop pending
211
- // for the ask timeout.
212
- if (cancelled) {
213
- return { ok: false, stopReason: "interrupted" };
214
- }
215
- // A remotely interrupted or failed run must stay stopped, even
216
- // if its partial output already contains a complete question.
217
- if (!ok)
218
- return { ok, stopReason };
219
- const questions = parseOmbAskQuestions(lastText);
220
- if (!questions) {
221
- // A fence that did not parse is a question the person never saw.
222
- // Say so on the next prompt instead of losing it silently. (An
223
- // over-cap block does not land here: the parser caps it at six
224
- // questions and still shows the card.)
225
- if (ASK_FENCE_ANY.test(lastText))
226
- malformedAsks.add(threadId);
227
- return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
228
- }
229
- const requestId = newId();
230
- let settleHeld;
231
- const held = new Promise((resolve) => {
232
- const timer = setTimeout(() => settleHeld(null, "timeout"), config.askTimeoutMs ?? 15 * 60_000);
233
- timer.unref?.();
234
- settleHeld = (reply, source) => {
235
- heldAsks.delete(threadId);
236
- clearTimeout(timer);
237
- resolve([reply, source]);
238
- };
239
- });
240
- // register before emitting — an answer can race the emit
241
- heldAsks.set(threadId, { requestId, settle: settleHeld });
242
- const choices = questionChoices(questions);
243
- emit({
244
- ...base(threadId, turnId),
245
- requestId,
246
- type: "request.opened",
247
- requestType: "question",
248
- tool: OMB_ASK_TOOL,
249
- summary: askQuestionSummary(questions),
250
- questions,
251
- ...(choices?.length ? { choices } : {}),
252
- origin: "output",
253
- });
254
- const [reply, source] = await held;
255
- emit({ ...base(threadId, turnId), requestId, type: "request.resolved", behavior: reply !== null ? "answer" : "deny", source });
256
- if (reply === null || cancelled) {
257
- return { ok: ok && !cancelled, stopReason: cancelled ? "interrupted" : stopReason };
258
- }
259
- // The turn is still open, so this prompt continues it: same turnId,
260
- // same accounting, and the answer reaches the boat the way the ask
261
- // contract promised — Q:/A: blocks, capped for echo.
262
- const continuation = [
263
- capAnswerEcho(answerWithoutPreamble(reply)),
264
- "",
265
- "The person answered the omb-ask questions above (Q:/A:). Continue the task with their answers; end with another omb-ask block only if you truly need more.",
266
- ].join("\n");
267
- const nextPromptId = await postPrompt(continuation);
268
- // Stop can land while the continuation POST is in flight: the
269
- // interrupt inside cancel() then hits a boat with no active run,
270
- // and the continuation would start after it. Interrupt the run
271
- // that just started before ending the turn.
272
- if (cancelled) {
273
- void api(`/boxes/${boxId}/interrupt`, { method: "POST" }).catch(() => { });
274
- return { ok: false, stopReason: "interrupted" };
275
- }
276
- return await settleRun(nextPromptId);
277
- };
278
- /** Poll one boat run to its settle. */
279
- const settleRun = async (runPromptId) => {
280
- const startedAt = Date.now(); // one 30-min ceiling per boat run, ask chains included
281
- lastText = "";
282
- pendingText = "";
283
- problem = null;
284
- for (;;) {
285
- if (cancelled)
286
- break;
287
- await new Promise((r) => setTimeout(r, config.pollMs));
288
- const events = await api(`/boxes/${boxId}/events`).catch(() => null);
289
- const list = events?.events ?? events?.items ?? [];
290
- for (const ev of list) {
291
- // The stream is the whole conversation from its start; `taskId`
292
- // names the prompt run an event belongs to. Earlier turns are
293
- // history, not this answer.
294
- if (runPromptId && ev.taskId && String(ev.taskId) !== runPromptId)
295
- continue;
296
- const id = String(ev.id ?? ev.eventId ?? JSON.stringify(ev).slice(0, 120));
297
- if (seen.has(id))
298
- continue;
299
- seen.add(id);
300
- appendNative(threadId, { dir: "in", source: "box.events", msg: ev });
301
- const kind = String(ev.type ?? ev.kind ?? "");
302
- // "response" events carry the agent's text at data.content —
303
- // the FULL text so far, not a chunk. Clients accumulate
304
- // deltas, so forward only the growth; a drifted (non-prefix)
305
- // event re-sends whole and the settled message replaces the
306
- // stream anyway.
307
- const text = ev.text ?? ev.message ?? ev.data?.text ?? ev.data?.content ?? null;
308
- if (/assistant|message|output|response/i.test(kind) && typeof text === "string" && text.trim()) {
309
- ingest(text);
310
- }
311
- else if (/usage_limit|error|fail/i.test(kind)) {
312
- const why = ev.data?.summary ?? ev.data?.message ?? ev.data?.error ?? ev.message;
313
- if (typeof why === "string" && why.trim())
314
- problem = why.trim();
315
- }
316
- else if (/tool|command|exec|browse/i.test(kind)) {
317
- flushAssistantText();
318
- emit({
319
- ...base(threadId, turnId),
320
- type: "item.started",
321
- itemType: "tool",
322
- itemId: id,
323
- title: String(ev.title ?? ev.command ?? kind).slice(0, 80),
324
- });
325
- }
326
- // shape-drift backstop: without a promptId the status poll
327
- // below can never see a terminal state, so settle off the
328
- // events themselves instead of hanging to the 30-min ceiling
329
- if (!runPromptId && /complete|finish|done|success|fail|error/i.test(kind)) {
330
- const failed = /fail|error/i.test(kind);
331
- return await finishRun(!failed, failed ? kind : null);
332
- }
333
- }
334
- if (runPromptId) {
335
- const status = await api(`/boxes/${boxId}/prompts/${runPromptId}`).catch(() => null);
336
- appendNative(threadId, { dir: "in", source: "box.prompt.status", msg: status });
337
- // real shape (2026-08): {promptRun:{status:"finished",…}} —
338
- // flat fallbacks kept for drift
339
- const run = status?.promptRun ?? status?.prompt ?? status ?? {};
340
- const state = String(run?.status ?? "");
341
- if (/completed|succeeded|done|finished/i.test(state)) {
342
- const result = run?.result ?? run?.output ?? lastText;
343
- if (typeof result === "string" && result.trim() && result !== lastText) {
344
- ingest(result);
345
- }
346
- if (!pendingText.trim() && !lastText.trim()) {
347
- // a run that ends with nothing said and a recorded problem
348
- // (login expired, …) is a failure the person must see
349
- if (problem)
350
- throw new Error(problem);
351
- pendingText = "(finished)";
352
- }
353
- return await finishRun(true, null);
354
- }
355
- if (/failed|error|cancelled|interrupted/i.test(state)) {
356
- const runError = [run?.error, run?.failureReason, run?.message].find((v) => typeof v === "string" && v.trim());
357
- if (problem || runError || /failed|error/i.test(state))
358
- throw new Error(problem ?? runError ?? `the box run ${state}`);
359
- return await finishRun(false, state);
360
- }
361
- }
362
- if (Date.now() - startedAt > 30 * 60_000) {
363
- throw new Error("box run exceeded 30 minutes — interrupted");
364
- }
365
- }
366
- // cancelled
367
- return await finishRun(false, "interrupted");
368
- };
369
- try {
370
- const outcome = await settleRun(promptId);
371
- active.delete(threadId);
372
- emit({ ...base(threadId, turnId), type: "turn.completed", ok: outcome.ok, stopReason: outcome.stopReason, cost: null });
373
- }
374
- catch (e) {
375
- flushAssistantText();
376
- heldAsks.delete(threadId);
377
- active.delete(threadId);
378
- emit({ ...base(threadId, turnId), type: "runtime.error", message: e.message });
379
- emit({ ...base(threadId, turnId), type: "turn.completed", ok: false, stopReason: "error", cost: null });
380
- }
381
- })();
382
- return { turnId };
383
- };
384
- const snapshot = async () => {
385
- if (!account()) {
386
- return { state: "unavailable", reason: 'no Boat token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
387
- }
388
- if (account()?.included)
389
- return { state: "unavailable", reason: COMPUTER_ENGINE_CLOUD_UNAVAILABLE };
390
- try {
391
- await api("/me");
392
- return { state: "available", authenticated: true, version: null };
393
- }
394
- catch (e) {
395
- return { state: "unavailable", reason: `box API unreachable: ${e.message}` };
396
- }
397
- };
398
- return {
399
- instanceId,
400
- driverKind: DRIVER_KIND,
401
- displayName: input.displayName,
402
- enabled: input.enabled,
403
- models: MODELS,
404
- snapshot,
405
- adapter: {
406
- provider: DRIVER_KIND,
407
- capabilities: { sessionModelSwitch: "in-session", remoteAgent: true },
408
- sendTurn,
409
- interruptTurn: async (threadId) => active.get(threadId)?.cancel(),
410
- respondToRequest: async (threadId, requestId, decision) => {
411
- const held = heldAsks.get(threadId);
412
- if (!held || held.requestId !== requestId)
413
- return "unavailable"; // settled, timed out, or restarted away
414
- const reply = decision.message?.trim();
415
- if (decision.behavior === "answer" && reply) {
416
- held.settle(decision.message, "user");
417
- return "answered";
418
- }
419
- if (decision.behavior === "deny") {
420
- held.settle(null, "user");
421
- return "rejected";
422
- }
423
- // "allow" is not an answer to a question, and blank text is not either
424
- return "unavailable";
425
- },
426
- hasSession: (threadId) => active.has(threadId),
427
- stopAll: async () => {
428
- for (const { cancel } of active.values())
429
- cancel();
430
- },
431
- onEvent: (listener) => {
432
- listeners.add(listener);
433
- return () => listeners.delete(listener);
434
- },
435
- },
436
- dispose: async () => {
437
- for (const { cancel } of active.values())
438
- cancel();
439
- listeners.clear();
440
- },
441
- };
442
- },
443
- };