openmausbot 0.1.78 → 0.1.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-Cu8BkIfo.js +305 -0
- package/dist/assets/{index-BqDJqf2C.js → index-D6RZKWks.js} +1 -1
- package/dist/assets/index-tfpeJAIG.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +11 -5
- package/dist-server/drivers/agents-proxy.js +21 -6
- package/dist-server/enterprise/server/index.js +0 -1
- package/dist-server/index.js +15942 -6859
- package/dist-server/local-computer.js +0 -1
- package/dist-server/mcp-server.js +8 -1
- package/dist-server/openmausbot.js +9303 -786
- package/dist-server/pair-cli.js +9303 -786
- package/dist-server/proxy-paths.js +0 -1
- package/dist-server/server/box.js +98 -41
- package/dist-server/server/browser-engine.js +1 -1
- package/dist-server/server/browser-runtime.js +11 -3
- package/dist-server/server/browser-tool-shape.js +72 -0
- package/dist-server/server/claude-accounts.js +1 -0
- package/dist-server/server/cli-setup.js +1 -1
- package/dist-server/server/composio.js +18 -0
- package/dist-server/server/config.js +16 -4
- package/dist-server/server/container-computer.js +0 -12
- package/dist-server/server/drivers/acp/core.js +4 -19
- package/dist-server/server/drivers/agents-proxy.js +27 -2
- package/dist-server/server/drivers/boxagent.js +48 -4
- package/dist-server/server/drivers/chat-mcp-tools.js +368 -0
- package/dist-server/server/drivers/chat-tool-approval.js +42 -0
- package/dist-server/server/drivers/claude.js +86 -38
- package/dist-server/server/drivers/codex.js +225 -75
- package/dist-server/server/drivers/grok.js +4 -0
- package/dist-server/server/drivers/minimax.js +11 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +92 -0
- package/dist-server/server/drivers/openai-chat.js +332 -88
- package/dist-server/server/drivers/openai-compat.js +4 -0
- package/dist-server/server/drivers/pi.js +1 -9
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +267 -50
- package/dist-server/server/managed-desktop.js +205 -0
- package/dist-server/server/model-context-window.js +20 -0
- package/dist-server/server/proxy-paths.js +0 -1
- package/dist-server/server/redact.js +1 -0
- package/dist-server/server/room-handoffs.js +57 -9
- package/dist-server/server/store.js +13 -1
- package/dist-server/server/tts/chatterbox.js +83 -0
- package/dist-server/server/tts/index.js +45 -12
- package/dist-server/server/workspace.js +12 -2
- package/dist-server/shared/ask-question.js +28 -0
- package/dist-server/shared/computer-contention.js +17 -0
- package/dist-server/vps-container-mcp.js +11 -5
- package/enterprise/server/index.js +0 -1
- package/package.json +1 -1
- package/dist/assets/index-DjwAroIZ.js +0 -305
- package/dist/assets/index-Pbb6Ao0s.css +0 -1
- package/dist-server/computer-proxy.js +0 -1196
- package/dist-server/server/computer-proxy.js +0 -1070
- package/dist-server/server/remote-computer.js +0 -170
|
@@ -12,24 +12,37 @@
|
|
|
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";
|
|
27
25
|
import { CodexDeviceAuthController } from "./codex-device-auth.js";
|
|
28
26
|
import { codexAccountEmail } from "./codex-identity.js";
|
|
27
|
+
import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
|
|
29
28
|
export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
30
29
|
const DRIVER_KIND = "codex";
|
|
31
30
|
const ASTRA_MODEL_ID = "gpt-6-astra";
|
|
32
31
|
const ASTRA_MIN_CODEX_VERSION = [0, 153, 1];
|
|
32
|
+
class CodexRpcError extends Error {
|
|
33
|
+
code;
|
|
34
|
+
constructor(error) {
|
|
35
|
+
super(error.message ?? JSON.stringify(error));
|
|
36
|
+
this.code = error.code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function missingNativeCodexThread(error, cursor) {
|
|
40
|
+
// Codex's local thread/resume rejection, verified with an empty native home.
|
|
41
|
+
// A generic 404, auth error, timeout or prose mentioning a missing thread is
|
|
42
|
+
// not evidence that the native history was lost. Unknown versions fail closed.
|
|
43
|
+
return error instanceof CodexRpcError && error.code === -32600 &&
|
|
44
|
+
error.message === `no rollout found for thread id ${cursor}`;
|
|
45
|
+
}
|
|
33
46
|
/** Whether an installed Codex predates the release that exposes GPT-6 Astra
|
|
34
47
|
* through app-server. Unknown version formats stay quiet: a bad guess should
|
|
35
48
|
* never nag someone whose custom build may already support the model. */
|
|
@@ -81,8 +94,33 @@ function decodeConfig(raw) {
|
|
|
81
94
|
return {
|
|
82
95
|
cli: typeof o.cli === "string" ? o.cli : "codex",
|
|
83
96
|
fullAuto: o.fullAuto === true,
|
|
97
|
+
...(o.managed && typeof o.managed === "object" ? { managed: decodeManagedCodex(o.managed) } : {}),
|
|
84
98
|
};
|
|
85
99
|
}
|
|
100
|
+
function decodeManagedCodex(raw) {
|
|
101
|
+
const value = raw;
|
|
102
|
+
if (typeof value.url !== "string" || !Array.isArray(value.models) || !value.models.length || value.models.some(model => typeof model !== "string" || !/^[\w][\w./+-]*$/.test(model))) {
|
|
103
|
+
throw new Error("Invalid Company Codex configuration.");
|
|
104
|
+
}
|
|
105
|
+
const url = new URL(value.url);
|
|
106
|
+
if (url.username || url.password || url.search || url.hash || !["https:", "http:"].includes(url.protocol))
|
|
107
|
+
throw new Error("Invalid Company Codex endpoint.");
|
|
108
|
+
return { url: url.href.replace(/\/$/, ""), models: value.models };
|
|
109
|
+
}
|
|
110
|
+
export function managedCodexArgs(config) {
|
|
111
|
+
// Credential stays in the instance environment, never argv or config.toml.
|
|
112
|
+
// https://learn.chatgpt.com/docs/config-file/config-reference
|
|
113
|
+
return [
|
|
114
|
+
"-c", 'model_provider="openmaus_company"',
|
|
115
|
+
"-c", 'model_providers.openmaus_company.name="Company"',
|
|
116
|
+
"-c", `model_providers.openmaus_company.base_url=${JSON.stringify(config.url)}`,
|
|
117
|
+
"-c", 'model_providers.openmaus_company.env_key="OPENMAUSBOT_COMPANY_API_KEY"',
|
|
118
|
+
"-c", 'model_providers.openmaus_company.wire_api="responses"',
|
|
119
|
+
"-c", "model_providers.openmaus_company.requires_openai_auth=false",
|
|
120
|
+
"-c", 'cli_auth_credentials_store="ephemeral"',
|
|
121
|
+
"-c", "shell_environment_policy.ignore_default_excludes=false",
|
|
122
|
+
];
|
|
123
|
+
}
|
|
86
124
|
const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
|
|
87
125
|
const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
|
|
88
126
|
/** RequestPermissionProfile uses null for permission families that were not
|
|
@@ -418,8 +456,10 @@ export const CodexDriver = {
|
|
|
418
456
|
return env;
|
|
419
457
|
};
|
|
420
458
|
const catalogEnv = childEnv();
|
|
421
|
-
let models = STATIC_CODEX_MODELS;
|
|
459
|
+
let models = config.managed ? { default: config.managed.models[0], options: config.managed.models.map(id => ({ id, label: id })) } : STATIC_CODEX_MODELS;
|
|
422
460
|
const refreshModels = async () => {
|
|
461
|
+
if (config.managed)
|
|
462
|
+
return;
|
|
423
463
|
try {
|
|
424
464
|
const resolved = await readCodexModelCatalog(catalogEnv, fetch, config.cli);
|
|
425
465
|
if (resolved.options.length)
|
|
@@ -449,9 +489,17 @@ export const CodexDriver = {
|
|
|
449
489
|
createdAt: new Date().toISOString(),
|
|
450
490
|
});
|
|
451
491
|
const sendTurn = async (turn) => {
|
|
492
|
+
if (config.managed && (!turn.model || !config.managed.models.includes(turn.model) || !input.environment.OPENMAUSBOT_COMPANY_API_KEY || !input.environment.CODEX_HOME)) {
|
|
493
|
+
throw new Error("Company model access is unavailable. Reconnect your organization; personal billing will not be used.");
|
|
494
|
+
}
|
|
452
495
|
// One driver instance serves many threads. Interrupt state belongs to
|
|
453
496
|
// this turn so activity elsewhere cannot cancel or revive its retry.
|
|
454
497
|
let stopRequested = false;
|
|
498
|
+
let promptSubmitted = false;
|
|
499
|
+
let recoveredMissingSession = false;
|
|
500
|
+
// Wakes a retry backoff the moment Stop arrives, so the turn settles
|
|
501
|
+
// now rather than after the full wait.
|
|
502
|
+
const stopSignal = new AbortController();
|
|
455
503
|
const { threadId } = turn;
|
|
456
504
|
// Direct adapter callers predating the per-bot selector retain the
|
|
457
505
|
// instance's legacy fullAuto setting. Harness turns always send an
|
|
@@ -472,30 +520,14 @@ export const CodexDriver = {
|
|
|
472
520
|
const retryScale = Number(process.env.FAKE_CODEX_RETRY_SCALE ?? "1");
|
|
473
521
|
const launchAttempt = async (attempt) => {
|
|
474
522
|
const env = childEnv();
|
|
475
|
-
const appServerArgs = ["app-server", ...codexLocalProviderArgs(env, turn.model)];
|
|
523
|
+
const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model))];
|
|
476
524
|
if (turn.integrations?.composio) {
|
|
477
525
|
mountMcpServer(appServerArgs, env, "openmausbot_connectors", turn.integrations.composio);
|
|
478
526
|
}
|
|
479
527
|
if (turn.integrations?.agents) {
|
|
480
528
|
mountMcpServer(appServerArgs, env, "agents", turn.integrations.agents);
|
|
481
529
|
}
|
|
482
|
-
if (turn.integrations?.
|
|
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) {
|
|
530
|
+
if (turn.integrations?.localComputer) {
|
|
499
531
|
// The host daemon and isolated Local VM both arrive as a direct Cua
|
|
500
532
|
// Driver stdio MCP server. Codex sees the same computer tool surface.
|
|
501
533
|
mountMcpServer(appServerArgs, env, "computer", turn.integrations.localComputer);
|
|
@@ -527,9 +559,12 @@ export const CodexDriver = {
|
|
|
527
559
|
lastError: "",
|
|
528
560
|
lastText: "",
|
|
529
561
|
sawStreamDelta: false,
|
|
530
|
-
// codex reports token usage as a running
|
|
531
|
-
// wants this turn's figure
|
|
562
|
+
// codex reports token usage as a running total for this app-server
|
|
563
|
+
// process. The harness wants this turn's figure: the total minus
|
|
564
|
+
// whatever the process already carried before turn/start (a resumed
|
|
565
|
+
// thread may restore earlier usage), banked on settle.
|
|
532
566
|
usage: undefined,
|
|
567
|
+
usageBaseline: undefined,
|
|
533
568
|
};
|
|
534
569
|
const asks = new Map();
|
|
535
570
|
let nextId = 1;
|
|
@@ -561,6 +596,18 @@ export const CodexDriver = {
|
|
|
561
596
|
rpcPending.set(id, {
|
|
562
597
|
resolve: (v) => {
|
|
563
598
|
clearTimeout(timer);
|
|
599
|
+
if (method === "thread/start" || method === "thread/resume") {
|
|
600
|
+
// Notifications can follow the thread response in the same
|
|
601
|
+
// stdout chunk, before the handshake await resumes.
|
|
602
|
+
const returnedId = v?.thread?.id;
|
|
603
|
+
const requestedId = method === "thread/resume"
|
|
604
|
+
&& params && typeof params === "object" && "threadId" in params
|
|
605
|
+
? params.threadId : null;
|
|
606
|
+
if (typeof returnedId === "string" && returnedId)
|
|
607
|
+
codexThreadId = returnedId;
|
|
608
|
+
else if (typeof requestedId === "string" && requestedId)
|
|
609
|
+
codexThreadId = requestedId;
|
|
610
|
+
}
|
|
564
611
|
if (method === "turn/start") {
|
|
565
612
|
if (typeof v?.turn?.id !== "string" || !v.turn.id) {
|
|
566
613
|
reject(new Error("Codex did not return a native turn id"));
|
|
@@ -594,6 +641,7 @@ export const CodexDriver = {
|
|
|
594
641
|
let completeStoppedTurn;
|
|
595
642
|
const stop = async () => {
|
|
596
643
|
stopRequested = true;
|
|
644
|
+
stopSignal.abort();
|
|
597
645
|
const stopped = await terminate();
|
|
598
646
|
if (stopped)
|
|
599
647
|
completeStoppedTurn?.();
|
|
@@ -744,6 +792,17 @@ export const CodexDriver = {
|
|
|
744
792
|
if (!connectionError) {
|
|
745
793
|
if (!codexThreadId || p.threadId !== codexThreadId)
|
|
746
794
|
return;
|
|
795
|
+
if (!codexTurnId && msg.method === "thread/tokenUsage/updated" && p.tokenUsage?.total) {
|
|
796
|
+
// A total reported before this turn exists is what the process
|
|
797
|
+
// carried in — a resumed thread restoring earlier usage. It is the
|
|
798
|
+
// baseline this turn's figure is measured from, never a reading to
|
|
799
|
+
// buffer and replay as if this turn produced it. (Codex names the
|
|
800
|
+
// turn before any model call, so a genuine first reading cannot
|
|
801
|
+
// land here.)
|
|
802
|
+
const t = p.tokenUsage.total;
|
|
803
|
+
state.usageBaseline = { input: t.inputTokens ?? 0, output: t.outputTokens ?? 0, cachedInput: t.cachedInputTokens ?? 0 };
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
747
806
|
if (!codexTurnId) {
|
|
748
807
|
// Some servers stream before acknowledging turn/start. Retain a
|
|
749
808
|
// bounded prefix, then filter against the authoritative response.
|
|
@@ -849,32 +908,44 @@ export const CodexDriver = {
|
|
|
849
908
|
break;
|
|
850
909
|
}
|
|
851
910
|
case "thread/tokenUsage/updated": {
|
|
852
|
-
// `
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
|
|
911
|
+
// `total` is everything this app-server process has used; `last`
|
|
912
|
+
// is the most recent model call. This turn's figure is the total
|
|
913
|
+
// minus what the process carried before turn/start went out (a
|
|
914
|
+
// resumed thread can restore earlier usage), so it never grows by
|
|
915
|
+
// the whole thread per message and never counts only the final
|
|
916
|
+
// call of a multi-step turn. codex's inputTokens already includes
|
|
917
|
+
// cachedInputTokens; the cached share rides alongside so the UI
|
|
918
|
+
// can say how much was context re-read rather than new text.
|
|
919
|
+
const t = p.tokenUsage?.total;
|
|
920
|
+
const last = p.tokenUsage?.last;
|
|
921
|
+
const shape = (u) => ({
|
|
922
|
+
input: u.inputTokens ?? 0, output: u.outputTokens ?? 0, cachedInput: u.cachedInputTokens ?? 0,
|
|
923
|
+
});
|
|
924
|
+
// (A total that arrived before this turn was named became the
|
|
925
|
+
// baseline upstream and never reaches this switch.)
|
|
926
|
+
if (t) {
|
|
927
|
+
const b = state.usageBaseline ?? { input: 0, output: 0, cachedInput: 0 };
|
|
928
|
+
const now = shape(t);
|
|
860
929
|
state.usage = {
|
|
861
|
-
input:
|
|
862
|
-
output:
|
|
863
|
-
...(typeof
|
|
864
|
-
? { cachedInput: turnUsage.cachedInputTokens }
|
|
865
|
-
: {}),
|
|
930
|
+
input: Math.max(0, now.input - b.input),
|
|
931
|
+
output: Math.max(0, now.output - b.output),
|
|
932
|
+
...(typeof t.cachedInputTokens === "number" ? { cachedInput: Math.max(0, now.cachedInput - b.cachedInput) } : {}),
|
|
866
933
|
};
|
|
867
934
|
}
|
|
868
|
-
|
|
935
|
+
else if (last) {
|
|
936
|
+
state.usage = { input: last.inputTokens ?? 0, output: last.outputTokens ?? 0, ...(typeof last.cachedInputTokens === "number" ? { cachedInput: last.cachedInputTokens } : {}) };
|
|
937
|
+
}
|
|
869
938
|
if (t) {
|
|
939
|
+
const window = p.tokenUsage?.modelContextWindow;
|
|
870
940
|
emit({
|
|
871
941
|
...base(threadId, turnId),
|
|
872
942
|
type: "thread.token-usage.updated",
|
|
873
943
|
input: t.inputTokens ?? 0,
|
|
874
944
|
output: t.outputTokens ?? 0,
|
|
875
|
-
...(typeof t.cachedInputTokens === "number"
|
|
876
|
-
|
|
877
|
-
|
|
945
|
+
...(typeof t.cachedInputTokens === "number" ? { cachedInput: t.cachedInputTokens } : {}),
|
|
946
|
+
// the last call's prompt is what fills the window
|
|
947
|
+
...(last && typeof last.inputTokens === "number" ? { contextTokens: last.inputTokens } : {}),
|
|
948
|
+
...(typeof window === "number" && window > 0 ? { contextWindow: window } : {}),
|
|
878
949
|
});
|
|
879
950
|
}
|
|
880
951
|
break;
|
|
@@ -926,6 +997,7 @@ export const CodexDriver = {
|
|
|
926
997
|
catch {
|
|
927
998
|
continue;
|
|
928
999
|
}
|
|
1000
|
+
stderrSinceOutput = "";
|
|
929
1001
|
const loggedMessage = codexNativeIncomingLogMessage(msg, sensitiveResponseIds);
|
|
930
1002
|
appendNative(threadId, { dir: "in", source: "codex.app-server", msg: loggedMessage });
|
|
931
1003
|
if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
|
|
@@ -933,7 +1005,7 @@ export const CodexDriver = {
|
|
|
933
1005
|
if (pend) {
|
|
934
1006
|
rpcPending.delete(msg.id);
|
|
935
1007
|
if (msg.error)
|
|
936
|
-
pend.reject(new
|
|
1008
|
+
pend.reject(new CodexRpcError(msg.error));
|
|
937
1009
|
else
|
|
938
1010
|
pend.resolve(msg.result);
|
|
939
1011
|
}
|
|
@@ -947,10 +1019,20 @@ export const CodexDriver = {
|
|
|
947
1019
|
}
|
|
948
1020
|
});
|
|
949
1021
|
let stderr = "";
|
|
1022
|
+
// Stderr that arrived after the last parsed protocol message. The
|
|
1023
|
+
// full buffer accumulates for the whole process lifetime, so its
|
|
1024
|
+
// tail can name a long-past event (a websocket 426 logged at turn
|
|
1025
|
+
// start, echoed half an hour later when something else kills the
|
|
1026
|
+
// process). Only this slice can explain an exit; older bytes are
|
|
1027
|
+
// context, not cause.
|
|
1028
|
+
let stderrSinceOutput = "";
|
|
950
1029
|
child.stderr.on("data", (c) => {
|
|
951
1030
|
stderr += c;
|
|
1031
|
+
stderrSinceOutput += c;
|
|
952
1032
|
if (stderr.length > 8192)
|
|
953
1033
|
stderr = stderr.slice(-8192);
|
|
1034
|
+
if (stderrSinceOutput.length > 2048)
|
|
1035
|
+
stderrSinceOutput = stderrSinceOutput.slice(-2048);
|
|
954
1036
|
});
|
|
955
1037
|
child.on("error", (e) => {
|
|
956
1038
|
if (abandoned)
|
|
@@ -958,7 +1040,7 @@ export const CodexDriver = {
|
|
|
958
1040
|
emit({ ...base(threadId, turnId), type: "runtime.error", ...describeSpawnFailure(e, config.cli) });
|
|
959
1041
|
void settle(false, "spawn_error");
|
|
960
1042
|
});
|
|
961
|
-
child.on("close", (code) => {
|
|
1043
|
+
child.on("close", (code, signal) => {
|
|
962
1044
|
if (abandoned)
|
|
963
1045
|
return;
|
|
964
1046
|
if (state.settled) {
|
|
@@ -967,14 +1049,65 @@ export const CodexDriver = {
|
|
|
967
1049
|
void stop();
|
|
968
1050
|
return;
|
|
969
1051
|
}
|
|
970
|
-
|
|
1052
|
+
// The child died before the turn completed. Attribute the exit
|
|
1053
|
+
// honestly: name the signal when it was killed, and only quote
|
|
1054
|
+
// stderr that arrived after the last protocol message. A stale
|
|
1055
|
+
// tail here once misattributed a whole day of killed turns to a
|
|
1056
|
+
// websocket 426 logged at turn start.
|
|
1057
|
+
const recentStderr = stderrSinceOutput.trim();
|
|
1058
|
+
const hadStreamedOutput = codexTurnId !== null || state.sawStreamDelta;
|
|
1059
|
+
// A signal exit is terminal no matter what the stderr says:
|
|
1060
|
+
// something killed the process (OOM, kill -9), and classifyError
|
|
1061
|
+
// cannot see the signal — with code null, transient-looking recent
|
|
1062
|
+
// stderr could still mark a killed attempt retryable.
|
|
1063
|
+
// Classification also reads only stderr received after the last
|
|
1064
|
+
// protocol output; the lifetime buffer's tail can name a
|
|
1065
|
+
// long-past event (the websocket-426 misattribution).
|
|
1066
|
+
const verdict = signal !== null
|
|
1067
|
+
? { transient: false, reason: "interrupted" }
|
|
1068
|
+
: classifyError({ exitCode: code, stderr: recentStderr });
|
|
1069
|
+
// Safe re-dispatch: relaunch only when the app-server never
|
|
1070
|
+
// acknowledged turn/start — no native turn began, nothing was
|
|
1071
|
+
// streamed, so replaying the input cannot duplicate work. After
|
|
1072
|
+
// any acknowledgement (or any buffered pre-ack event) the turn
|
|
1073
|
+
// settles instead: a replay could re-run tools the user saw.
|
|
1074
|
+
if (!stopRequested && codexTurnId === null && earlyNotifications.length === 0 &&
|
|
1075
|
+
verdict.transient && attempt < RETRY_MAX_ATTEMPTS - 1) {
|
|
1076
|
+
const delayMs = computeBackoff(attempt);
|
|
1077
|
+
attempt++;
|
|
971
1078
|
emit({
|
|
972
1079
|
...base(threadId, turnId),
|
|
973
|
-
type: "
|
|
974
|
-
|
|
1080
|
+
type: "turn.retrying",
|
|
1081
|
+
attempt,
|
|
1082
|
+
delayMs,
|
|
1083
|
+
reason: verdict.reason,
|
|
975
1084
|
});
|
|
976
|
-
|
|
1085
|
+
// Retire this attempt before anything async runs, so a late
|
|
1086
|
+
// rpc timer rejection in the handshake catch cannot relaunch
|
|
1087
|
+
// a second time on top of this one.
|
|
1088
|
+
abandoned = true;
|
|
1089
|
+
void (async () => {
|
|
1090
|
+
const alreadyDead = child.exitCode !== null || child.signalCode !== null;
|
|
1091
|
+
if (!alreadyDead && !(await terminate())) {
|
|
1092
|
+
void settle(false, "shutdown_timeout");
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
await interruptibleDelay(Math.max(1, Math.round(delayMs * retryScale)), stopSignal.signal).promise;
|
|
1096
|
+
if (!stopRequested) {
|
|
1097
|
+
void launchAttempt(attempt).catch(() => { });
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
await settle(false, "interrupted");
|
|
1101
|
+
}
|
|
1102
|
+
})().catch(() => { });
|
|
1103
|
+
return;
|
|
977
1104
|
}
|
|
1105
|
+
emit({
|
|
1106
|
+
...base(threadId, turnId),
|
|
1107
|
+
type: "runtime.error",
|
|
1108
|
+
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" : ""}`,
|
|
1109
|
+
});
|
|
1110
|
+
void settle(false, "exit_before_result");
|
|
978
1111
|
});
|
|
979
1112
|
active.set(threadId, { stop, turnId, asks });
|
|
980
1113
|
// Relaunching the app-server is still the same logical turn. Keep the
|
|
@@ -1030,35 +1163,48 @@ export const CodexDriver = {
|
|
|
1030
1163
|
// Removed bot rules are cleared without dropping native configured rules.
|
|
1031
1164
|
const cursor = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
1032
1165
|
let startedModel = null;
|
|
1166
|
+
let resumedNativeThread = false;
|
|
1167
|
+
let promptText = turn.text;
|
|
1033
1168
|
if (cursor) {
|
|
1169
|
+
const resumeThread = () => request("thread/resume", {
|
|
1170
|
+
threadId: cursor,
|
|
1171
|
+
developerInstructions,
|
|
1172
|
+
...approvalParams.thread,
|
|
1173
|
+
});
|
|
1034
1174
|
try {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1175
|
+
let resumed;
|
|
1176
|
+
try {
|
|
1177
|
+
resumed = await resumeThread();
|
|
1178
|
+
}
|
|
1179
|
+
catch (error) {
|
|
1180
|
+
if (!approvalParams.fallback || !permissionProfileUnsupported(error))
|
|
1181
|
+
throw error;
|
|
1182
|
+
// Older servers may require the legacy permission selector, but
|
|
1183
|
+
// still resume the same native thread before any user submission.
|
|
1184
|
+
approvalParams = approvalParams.fallback;
|
|
1185
|
+
resumed = await resumeThread();
|
|
1186
|
+
}
|
|
1040
1187
|
codexThreadId = resumed?.thread?.id ?? cursor;
|
|
1188
|
+
resumedNativeThread = true;
|
|
1041
1189
|
}
|
|
1042
1190
|
catch (error) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
...approvalParams.thread,
|
|
1052
|
-
});
|
|
1053
|
-
codexThreadId = resumed?.thread?.id ?? cursor;
|
|
1054
|
-
}
|
|
1055
|
-
else {
|
|
1191
|
+
const failure = classifyResumeFailure({
|
|
1192
|
+
attempted: true,
|
|
1193
|
+
rejected: error instanceof CodexRpcError,
|
|
1194
|
+
promptSubmitted,
|
|
1195
|
+
producedOutput: state.sawStreamDelta,
|
|
1196
|
+
});
|
|
1197
|
+
if (!config.managed || recoveredMissingSession || stopRequested || state.settled ||
|
|
1198
|
+
!turn.recoveryText?.trim() || !missingNativeCodexThread(error, cursor) || !mayReplay(failure))
|
|
1056
1199
|
throw error;
|
|
1057
|
-
|
|
1200
|
+
// The prompt has never been submitted. Rebuild only missing Company
|
|
1201
|
+
// histories, once, through the same approved model/provider below.
|
|
1202
|
+
recoveredMissingSession = true;
|
|
1203
|
+
promptText = recoveryPromptFor({ recoveryText: turn.recoveryText, currentText: turn.text, failure }).text;
|
|
1058
1204
|
}
|
|
1059
1205
|
}
|
|
1060
1206
|
if (!codexThreadId) {
|
|
1061
|
-
const selection = decodeCodexSelection(turn.model);
|
|
1207
|
+
const selection = config.managed ? { model: turn.model, modelProvider: "openmaus_company" } : decodeCodexSelection(turn.model);
|
|
1062
1208
|
const startThread = () => request("thread/start", {
|
|
1063
1209
|
developerInstructions,
|
|
1064
1210
|
cwd: turn.cwd ?? homedir(),
|
|
@@ -1082,14 +1228,14 @@ export const CodexDriver = {
|
|
|
1082
1228
|
}
|
|
1083
1229
|
if (!codexThreadId)
|
|
1084
1230
|
throw new Error("Codex did not return a native thread id");
|
|
1085
|
-
await syncCodexInstructions(threadId, codexThreadId, developerInstructions,
|
|
1231
|
+
await syncCodexInstructions(threadId, codexThreadId, developerInstructions, resumedNativeThread, request);
|
|
1086
1232
|
emit({ ...base(threadId, turnId), type: "session.started", sessionId: codexThreadId, model: startedModel ?? turn.model ?? null });
|
|
1087
|
-
const promptText = turn.text;
|
|
1088
1233
|
const turnInput = [
|
|
1089
1234
|
...(promptText ? [{ type: "text", text: promptText }] : []),
|
|
1090
1235
|
...(turn.images ?? []).map((image) => ({ type: "localImage", path: image.path })),
|
|
1091
1236
|
];
|
|
1092
1237
|
const startTurn = () => {
|
|
1238
|
+
promptSubmitted = true;
|
|
1093
1239
|
startingNativeTurn = true;
|
|
1094
1240
|
return request("turn/start", {
|
|
1095
1241
|
threadId: codexThreadId,
|
|
@@ -1122,7 +1268,10 @@ export const CodexDriver = {
|
|
|
1122
1268
|
const message = e instanceof Error ? e.message : String(e);
|
|
1123
1269
|
const needsAuth = /(?:\b401\b|unauthorized|missing bearer|authentication required)/i.test(message);
|
|
1124
1270
|
const verdict = classifyError(failure);
|
|
1125
|
-
|
|
1271
|
+
// Three guards hold here: main's abandoned attempt never retries,
|
|
1272
|
+
// neither does a Company session already recovered once from canonical
|
|
1273
|
+
// history, and a Stop already asked for must not be undone by a relaunch.
|
|
1274
|
+
if (!state.settled && !abandoned && !recoveredMissingSession && !stopRequested && !needsAuth && verdict.transient && attempt < RETRY_MAX_ATTEMPTS - 1 && state.sawStreamDelta === false) {
|
|
1126
1275
|
const delayMs = computeBackoff(attempt);
|
|
1127
1276
|
attempt++;
|
|
1128
1277
|
emit({
|
|
@@ -1139,10 +1288,7 @@ export const CodexDriver = {
|
|
|
1139
1288
|
void settle(false, "shutdown_timeout");
|
|
1140
1289
|
return;
|
|
1141
1290
|
}
|
|
1142
|
-
await
|
|
1143
|
-
const timer = setTimeout(resolve, Math.max(1, Math.round(delayMs * retryScale)));
|
|
1144
|
-
timer.unref?.();
|
|
1145
|
-
});
|
|
1291
|
+
await interruptibleDelay(Math.max(1, Math.round(delayMs * retryScale)), stopSignal.signal).promise;
|
|
1146
1292
|
if (!stopRequested) {
|
|
1147
1293
|
void launchAttempt(attempt).catch(() => { });
|
|
1148
1294
|
}
|
|
@@ -1151,7 +1297,9 @@ export const CodexDriver = {
|
|
|
1151
1297
|
}
|
|
1152
1298
|
return;
|
|
1153
1299
|
}
|
|
1154
|
-
|
|
1300
|
+
// abandoned marks an attempt retired by a retry; its late rpc
|
|
1301
|
+
// timeouts must neither report a spurious error nor relaunch again
|
|
1302
|
+
if (!state.settled && !abandoned) {
|
|
1155
1303
|
emit({
|
|
1156
1304
|
...base(threadId, turnId),
|
|
1157
1305
|
type: "runtime.error",
|
|
@@ -1172,6 +1320,8 @@ export const CodexDriver = {
|
|
|
1172
1320
|
});
|
|
1173
1321
|
if (!version)
|
|
1174
1322
|
return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
|
|
1323
|
+
if (config.managed)
|
|
1324
|
+
return { state: "available", version, authenticated: Boolean(input.environment.OPENMAUSBOT_COMPANY_API_KEY && input.environment.CODEX_HOME), billing: "metered" };
|
|
1175
1325
|
const authenticated = await new Promise((resolve) => {
|
|
1176
1326
|
execCli(config.cli, ["login", "status"], { timeout: 8000, env }, (err, stdout, stderr) => resolve(!err && /^logged in\b/im.test(`${stdout}\n${stderr ?? ""}`)));
|
|
1177
1327
|
});
|
|
@@ -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",
|