openmausbot 0.1.78 → 0.1.79

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 (47) hide show
  1. package/dist/assets/{index-BqDJqf2C.js → index-C2Je5jdF.js} +1 -1
  2. package/dist/assets/index-D_TFZLlo.js +305 -0
  3. package/dist/index.html +1 -1
  4. package/dist-server/container-mcp.js +11 -5
  5. package/dist-server/enterprise/server/index.js +0 -1
  6. package/dist-server/index.js +15451 -6812
  7. package/dist-server/local-computer.js +0 -1
  8. package/dist-server/mcp-server.js +8 -1
  9. package/dist-server/openmausbot.js +9190 -758
  10. package/dist-server/pair-cli.js +9190 -758
  11. package/dist-server/proxy-paths.js +0 -1
  12. package/dist-server/server/box.js +98 -41
  13. package/dist-server/server/browser-engine.js +1 -1
  14. package/dist-server/server/browser-runtime.js +11 -3
  15. package/dist-server/server/browser-tool-shape.js +72 -0
  16. package/dist-server/server/claude-accounts.js +1 -0
  17. package/dist-server/server/cli-setup.js +1 -1
  18. package/dist-server/server/composio.js +18 -0
  19. package/dist-server/server/config.js +16 -4
  20. package/dist-server/server/container-computer.js +0 -12
  21. package/dist-server/server/drivers/acp/core.js +4 -19
  22. package/dist-server/server/drivers/boxagent.js +48 -4
  23. package/dist-server/server/drivers/chat-mcp-tools.js +368 -0
  24. package/dist-server/server/drivers/chat-tool-approval.js +42 -0
  25. package/dist-server/server/drivers/claude.js +87 -32
  26. package/dist-server/server/drivers/codex.js +69 -31
  27. package/dist-server/server/drivers/grok.js +4 -0
  28. package/dist-server/server/drivers/minimax.js +11 -3
  29. package/dist-server/server/drivers/openai-chat-protocol.js +92 -0
  30. package/dist-server/server/drivers/openai-chat.js +332 -88
  31. package/dist-server/server/drivers/openai-compat.js +4 -0
  32. package/dist-server/server/drivers/pi.js +1 -9
  33. package/dist-server/server/index.js +64 -38
  34. package/dist-server/server/proxy-paths.js +0 -1
  35. package/dist-server/server/room-handoffs.js +47 -9
  36. package/dist-server/server/tts/chatterbox.js +83 -0
  37. package/dist-server/server/tts/index.js +45 -12
  38. package/dist-server/server/workspace.js +12 -2
  39. package/dist-server/shared/ask-question.js +28 -0
  40. package/dist-server/shared/computer-contention.js +17 -0
  41. package/dist-server/vps-container-mcp.js +11 -5
  42. package/enterprise/server/index.js +0 -1
  43. package/package.json +1 -1
  44. package/dist/assets/index-DjwAroIZ.js +0 -305
  45. package/dist-server/computer-proxy.js +0 -1196
  46. package/dist-server/server/computer-proxy.js +0 -1070
  47. package/dist-server/server/remote-computer.js +0 -170
@@ -12,15 +12,13 @@
12
12
  import { existsSync } from "node:fs";
13
13
  import { homedir } from "node:os";
14
14
  import { stripWorkspaceCredentialEnv } from "../config.js";
15
- import { computerProxyEnv } from "../container-computer.js";
16
15
  import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
17
- import { SPAWNED_PROXIES } from "../proxy-paths.js";
18
16
  import { isHarnessOwnedMcpEnvName } from "../mcp-registry.js";
19
17
  import { newEventId, newId } from "../contracts.js";
20
18
  import { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
21
19
  import { codexLocalProviderArgs } from "./local-inject.js";
22
20
  import { augmentedPath, splitCliString } from "../env-path.js";
23
- import { classifyError, computeBackoff, RETRY_MAX_ATTEMPTS } from "./retry.js";
21
+ import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
24
22
  import { appendNative } from "./native.js";
25
23
  import { commandSummary, toolDetailPreview } from "../tool-summary.js";
26
24
  import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
@@ -452,6 +450,9 @@ export const CodexDriver = {
452
450
  // One driver instance serves many threads. Interrupt state belongs to
453
451
  // this turn so activity elsewhere cannot cancel or revive its retry.
454
452
  let stopRequested = false;
453
+ // Wakes a retry backoff the moment Stop arrives, so the turn settles
454
+ // now rather than after the full wait.
455
+ const stopSignal = new AbortController();
455
456
  const { threadId } = turn;
456
457
  // Direct adapter callers predating the per-bot selector retain the
457
458
  // instance's legacy fullAuto setting. Harness turns always send an
@@ -479,23 +480,7 @@ export const CodexDriver = {
479
480
  if (turn.integrations?.agents) {
480
481
  mountMcpServer(appServerArgs, env, "agents", turn.integrations.agents);
481
482
  }
482
- if (turn.integrations?.computer) {
483
- const proxyEnv = computerProxyEnv(turn.integrations.computer);
484
- mountMcpServer(appServerArgs, env, "computer", {
485
- command: process.execPath,
486
- args: [SPAWNED_PROXIES.computer],
487
- env: {
488
- ELECTRON_RUN_AS_NODE: "1",
489
- OGB_BOX_ID: proxyEnv.OGB_BOX_ID ?? "",
490
- OGB_BOX_TOKEN: proxyEnv.OGB_BOX_TOKEN ?? "",
491
- // who-is-driving endpoint, so a person taking the wheel in the
492
- // panel pauses this bot's hands mid-turn
493
- OMB_CONTROL_URL: proxyEnv.OMB_CONTROL_URL ?? "",
494
- OMB_CONTROL_TOKEN: proxyEnv.OMB_CONTROL_TOKEN ?? "",
495
- },
496
- });
497
- }
498
- else if (turn.integrations?.localComputer) {
483
+ if (turn.integrations?.localComputer) {
499
484
  // The host daemon and isolated Local VM both arrive as a direct Cua
500
485
  // Driver stdio MCP server. Codex sees the same computer tool surface.
501
486
  mountMcpServer(appServerArgs, env, "computer", turn.integrations.localComputer);
@@ -594,6 +579,7 @@ export const CodexDriver = {
594
579
  let completeStoppedTurn;
595
580
  const stop = async () => {
596
581
  stopRequested = true;
582
+ stopSignal.abort();
597
583
  const stopped = await terminate();
598
584
  if (stopped)
599
585
  completeStoppedTurn?.();
@@ -926,6 +912,7 @@ export const CodexDriver = {
926
912
  catch {
927
913
  continue;
928
914
  }
915
+ stderrSinceOutput = "";
929
916
  const loggedMessage = codexNativeIncomingLogMessage(msg, sensitiveResponseIds);
930
917
  appendNative(threadId, { dir: "in", source: "codex.app-server", msg: loggedMessage });
931
918
  if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
@@ -947,10 +934,20 @@ export const CodexDriver = {
947
934
  }
948
935
  });
949
936
  let stderr = "";
937
+ // Stderr that arrived after the last parsed protocol message. The
938
+ // full buffer accumulates for the whole process lifetime, so its
939
+ // tail can name a long-past event (a websocket 426 logged at turn
940
+ // start, echoed half an hour later when something else kills the
941
+ // process). Only this slice can explain an exit; older bytes are
942
+ // context, not cause.
943
+ let stderrSinceOutput = "";
950
944
  child.stderr.on("data", (c) => {
951
945
  stderr += c;
946
+ stderrSinceOutput += c;
952
947
  if (stderr.length > 8192)
953
948
  stderr = stderr.slice(-8192);
949
+ if (stderrSinceOutput.length > 2048)
950
+ stderrSinceOutput = stderrSinceOutput.slice(-2048);
954
951
  });
955
952
  child.on("error", (e) => {
956
953
  if (abandoned)
@@ -958,7 +955,7 @@ export const CodexDriver = {
958
955
  emit({ ...base(threadId, turnId), type: "runtime.error", ...describeSpawnFailure(e, config.cli) });
959
956
  void settle(false, "spawn_error");
960
957
  });
961
- child.on("close", (code) => {
958
+ child.on("close", (code, signal) => {
962
959
  if (abandoned)
963
960
  return;
964
961
  if (state.settled) {
@@ -967,14 +964,56 @@ export const CodexDriver = {
967
964
  void stop();
968
965
  return;
969
966
  }
970
- if (!state.settled) {
967
+ // The child died before the turn completed. Attribute the exit
968
+ // honestly: name the signal when it was killed, and only quote
969
+ // stderr that arrived after the last protocol message. A stale
970
+ // tail here once misattributed a whole day of killed turns to a
971
+ // websocket 426 logged at turn start.
972
+ const recentStderr = stderrSinceOutput.trim();
973
+ const hadStreamedOutput = codexTurnId !== null || state.sawStreamDelta;
974
+ const verdict = classifyError({ exitCode: code, stderr: recentStderr || stderr });
975
+ // Safe re-dispatch: relaunch only when the app-server never
976
+ // acknowledged turn/start — no native turn began, nothing was
977
+ // streamed, so replaying the input cannot duplicate work. After
978
+ // any acknowledgement (or any buffered pre-ack event) the turn
979
+ // settles instead: a replay could re-run tools the user saw.
980
+ if (!stopRequested && codexTurnId === null && earlyNotifications.length === 0 &&
981
+ verdict.transient && attempt < RETRY_MAX_ATTEMPTS - 1) {
982
+ const delayMs = computeBackoff(attempt);
983
+ attempt++;
971
984
  emit({
972
985
  ...base(threadId, turnId),
973
- type: "runtime.error",
974
- message: `codex exited ${code} before turn/completed${stderr ? `: ${stderr.trim().slice(-300)}` : ""}`,
986
+ type: "turn.retrying",
987
+ attempt,
988
+ delayMs,
989
+ reason: verdict.reason,
975
990
  });
976
- void settle(false, "exit_before_result");
991
+ // Retire this attempt before anything async runs, so a late
992
+ // rpc timer rejection in the handshake catch cannot relaunch
993
+ // a second time on top of this one.
994
+ abandoned = true;
995
+ void (async () => {
996
+ const alreadyDead = child.exitCode !== null || child.signalCode !== null;
997
+ if (!alreadyDead && !(await terminate())) {
998
+ void settle(false, "shutdown_timeout");
999
+ return;
1000
+ }
1001
+ await interruptibleDelay(Math.max(1, Math.round(delayMs * retryScale)), stopSignal.signal).promise;
1002
+ if (!stopRequested) {
1003
+ void launchAttempt(attempt).catch(() => { });
1004
+ }
1005
+ else {
1006
+ await settle(false, "interrupted");
1007
+ }
1008
+ })().catch(() => { });
1009
+ return;
977
1010
  }
1011
+ emit({
1012
+ ...base(threadId, turnId),
1013
+ type: "runtime.error",
1014
+ message: `codex exited ${code}${signal ? ` (signal ${signal})` : ""} before turn/completed${recentStderr ? `: ${recentStderr.slice(-300)}` : hadStreamedOutput && stderr.trim() ? "; no stderr after the last app-server output" : ""}`,
1015
+ });
1016
+ void settle(false, "exit_before_result");
978
1017
  });
979
1018
  active.set(threadId, { stop, turnId, asks });
980
1019
  // Relaunching the app-server is still the same logical turn. Keep the
@@ -1122,7 +1161,7 @@ export const CodexDriver = {
1122
1161
  const message = e instanceof Error ? e.message : String(e);
1123
1162
  const needsAuth = /(?:\b401\b|unauthorized|missing bearer|authentication required)/i.test(message);
1124
1163
  const verdict = classifyError(failure);
1125
- if (!state.settled && !needsAuth && verdict.transient && attempt < RETRY_MAX_ATTEMPTS - 1 && state.sawStreamDelta === false) {
1164
+ if (!state.settled && !abandoned && !needsAuth && verdict.transient && attempt < RETRY_MAX_ATTEMPTS - 1 && state.sawStreamDelta === false) {
1126
1165
  const delayMs = computeBackoff(attempt);
1127
1166
  attempt++;
1128
1167
  emit({
@@ -1139,10 +1178,7 @@ export const CodexDriver = {
1139
1178
  void settle(false, "shutdown_timeout");
1140
1179
  return;
1141
1180
  }
1142
- await new Promise((resolve) => {
1143
- const timer = setTimeout(resolve, Math.max(1, Math.round(delayMs * retryScale)));
1144
- timer.unref?.();
1145
- });
1181
+ await interruptibleDelay(Math.max(1, Math.round(delayMs * retryScale)), stopSignal.signal).promise;
1146
1182
  if (!stopRequested) {
1147
1183
  void launchAttempt(attempt).catch(() => { });
1148
1184
  }
@@ -1151,7 +1187,9 @@ export const CodexDriver = {
1151
1187
  }
1152
1188
  return;
1153
1189
  }
1154
- if (!state.settled) {
1190
+ // abandoned marks an attempt retired by a retry; its late rpc
1191
+ // timeouts must neither report a spurious error nor relaunch again
1192
+ if (!state.settled && !abandoned) {
1155
1193
  emit({
1156
1194
  ...base(threadId, turnId),
1157
1195
  type: "runtime.error",
@@ -11,7 +11,10 @@ const MODELS = {
11
11
  };
12
12
  function decodeConfig(raw) {
13
13
  const config = (raw ?? {});
14
+ if (config.tools !== undefined && typeof config.tools !== "boolean")
15
+ throw new Error("tools must be a boolean");
14
16
  return {
17
+ ...(config.tools !== undefined ? { tools: config.tools } : {}),
15
18
  url: typeof config.url === "string" ? config.url : DEFAULT_URL,
16
19
  apiKeyEnv: typeof config.apiKeyEnv === "string" ? config.apiKeyEnv : "XAI_API_KEY",
17
20
  };
@@ -30,6 +33,7 @@ export const GrokDriver = {
30
33
  driverKind: DRIVER_KIND,
31
34
  apiKey,
32
35
  apiUrl: config.url,
36
+ tools: config.tools,
33
37
  models: () => MODELS,
34
38
  requestBody: (model, messages, stream) => ({ model, messages, stream }),
35
39
  httpErrorLabel: "xAI",
@@ -11,10 +11,12 @@ const DEFAULT_URL = "https://api.minimax.io/v1";
11
11
  const CN_URL = "https://api.minimaxi.com/v1";
12
12
  const MODELS = {
13
13
  default: "MiniMax-M3",
14
+ // `custom: true` because the driver is access "custom": the picker opens
15
+ // custom-access engines in the pane that lists only custom-flagged models.
14
16
  options: [
15
- { id: "MiniMax-M3", label: "MiniMax M3", contextWindow: 1_000_000 },
16
- { id: "MiniMax-M2.7", label: "MiniMax M2.7", contextWindow: 204_800 },
17
- { id: "MiniMax-M2.7-highspeed", label: "MiniMax M2.7 Highspeed", contextWindow: 204_800 },
17
+ { id: "MiniMax-M3", label: "MiniMax M3", contextWindow: 1_000_000, custom: true },
18
+ { id: "MiniMax-M2.7", label: "MiniMax M2.7", contextWindow: 204_800, custom: true },
19
+ { id: "MiniMax-M2.7-highspeed", label: "MiniMax M2.7 Highspeed", contextWindow: 204_800, custom: true },
18
20
  ],
19
21
  };
20
22
  const localConfigSchema = z.object({
@@ -45,9 +47,13 @@ export function loadLocalMiniMaxConfig(home = homedir()) {
45
47
  }
46
48
  }
47
49
  export function decodeMinimaxConfig(raw) {
50
+ const tools = (raw ?? {}).tools;
51
+ if (tools !== undefined && typeof tools !== "boolean")
52
+ throw new Error("tools must be a boolean");
48
53
  const parsed = driverConfigSchema.safeParse(raw ?? {});
49
54
  const config = parsed.success ? parsed.data : {};
50
55
  return {
56
+ ...(tools !== undefined ? { tools } : {}),
51
57
  url: normalizedApiUrl(config.url?.trim() || process.env.MINIMAX_BASE_URL?.trim() || DEFAULT_URL),
52
58
  };
53
59
  }
@@ -83,6 +89,7 @@ export const MinimaxDriver = {
83
89
  driverKind: DRIVER_KIND,
84
90
  apiKey,
85
91
  apiUrl,
92
+ tools: input.config.tools,
86
93
  models: () => models,
87
94
  requestBody: (model, messages, stream) => ({
88
95
  model,
@@ -95,6 +102,7 @@ export const MinimaxDriver = {
95
102
  missingKeyError: `no MiniMax key — set ${API_KEY_ENV} or run mmx auth login --api-key …`,
96
103
  unavailableReason: `no MiniMax API key — run mmx auth login --api-key … or set ${API_KEY_ENV}`,
97
104
  timeoutMs: 180_000,
105
+ reasoning: true,
98
106
  billing: "metered",
99
107
  includeUsageInCompleted: true,
100
108
  noBodyError: "MiniMax returned no response body",
@@ -0,0 +1,92 @@
1
+ export const MAX_CHAT_TOOL_CALLS = 32;
2
+ const MAX_ARGUMENT_CHARS = 256_000;
3
+ export class ChatProtocolError extends Error {
4
+ }
5
+ export function object(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value)
7
+ ? value : undefined;
8
+ }
9
+ /** Opaque provider reasoning blocks must keep their original order/signatures.
10
+ * OpenRouter streams the sequence as arrays of blocks, not tool-call deltas. */
11
+ export class ChatReasoningDetails {
12
+ blocks = [];
13
+ chars = 0;
14
+ add(value) {
15
+ if (value === undefined || value === null)
16
+ return;
17
+ if (!Array.isArray(value) || value.some((block) => !object(block))) {
18
+ throw new ChatProtocolError("provider returned invalid reasoning details");
19
+ }
20
+ this.chars += JSON.stringify(value).length;
21
+ if (this.chars > 256_000)
22
+ throw new ChatProtocolError("provider reasoning details exceeded the size limit");
23
+ this.blocks.push(...value);
24
+ }
25
+ }
26
+ /** One accumulator per completion: indexes identify deltas; IDs correlate results. */
27
+ export class ChatToolCalls {
28
+ calls = new Map();
29
+ argumentChars = 0;
30
+ add(value, streaming) {
31
+ if (value === undefined || value === null)
32
+ return;
33
+ if (!Array.isArray(value))
34
+ throw new ChatProtocolError("provider returned invalid tool_calls");
35
+ for (const [position, raw] of value.entries()) {
36
+ const delta = object(raw);
37
+ const index = streaming ? delta?.index : position;
38
+ if (!delta || !Number.isInteger(index) || Number(index) < 0 || Number(index) >= MAX_CHAT_TOOL_CALLS) {
39
+ throw new ChatProtocolError("provider returned an invalid tool-call index");
40
+ }
41
+ const call = this.calls.get(Number(index)) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
42
+ if (delta.type !== undefined && delta.type !== "function")
43
+ throw new ChatProtocolError("unsupported tool-call type");
44
+ if (delta.id !== undefined) {
45
+ if (typeof delta.id !== "string" || !delta.id || delta.id.length > 256 || (call.id && call.id !== delta.id)) {
46
+ throw new ChatProtocolError("provider changed or omitted a tool-call ID");
47
+ }
48
+ call.id = delta.id;
49
+ }
50
+ if (delta.function !== undefined) {
51
+ const fn = object(delta.function);
52
+ if (!fn)
53
+ throw new ChatProtocolError("provider returned an invalid tool-call function");
54
+ for (const field of ["name", "arguments"]) {
55
+ if (fn[field] === undefined)
56
+ continue;
57
+ if (typeof fn[field] !== "string")
58
+ throw new ChatProtocolError("provider returned invalid tool-call arguments or name");
59
+ call.function[field] += fn[field];
60
+ if (field === "arguments")
61
+ this.argumentChars += fn[field].length;
62
+ }
63
+ }
64
+ if (call.function.name.length > 64 || this.argumentChars > MAX_ARGUMENT_CHARS) {
65
+ throw new ChatProtocolError("provider tool calls exceeded the size limit");
66
+ }
67
+ this.calls.set(Number(index), call);
68
+ }
69
+ }
70
+ finish(reason, malformedFrame) {
71
+ const calls = [...this.calls.entries()].sort(([a], [b]) => a - b).map(([, call]) => call);
72
+ if (malformedFrame)
73
+ throw new ChatProtocolError("provider stream contained a malformed completion frame");
74
+ if (reason === "function_call")
75
+ throw new ChatProtocolError("legacy function_call is unsupported; use structured tool_calls");
76
+ if (reason === "tool_calls" && !calls.length)
77
+ throw new ChatProtocolError("provider ended for tool calls without a complete call");
78
+ if (!calls.length)
79
+ return calls;
80
+ if (reason !== null && reason !== "tool_calls" && reason !== "stop") {
81
+ throw new ChatProtocolError("provider tool calls were truncated or malformed");
82
+ }
83
+ const ids = new Set();
84
+ for (const call of calls) {
85
+ if (!call.id || ids.has(call.id) || !/^[A-Za-z0-9_-]{1,64}$/.test(call.function.name)) {
86
+ throw new ChatProtocolError("provider returned an incomplete or duplicate tool call");
87
+ }
88
+ ids.add(call.id);
89
+ }
90
+ return calls;
91
+ }
92
+ }