openmausbot 0.1.79 → 0.1.81
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/README.md +6 -5
- package/dist/assets/index-CKysBq-V.css +1 -0
- package/dist/assets/{index-C2Je5jdF.js → index-oLMljotU.js} +1 -1
- package/dist/assets/index-peLFiSH8.js +307 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +41 -10
- package/dist-server/drivers/agents-proxy.js +71 -17
- package/dist-server/index.js +4104 -1505
- package/dist-server/openmausbot.js +554 -163
- package/dist-server/pair-cli.js +554 -163
- package/dist-server/server/box-delete-journal.js +471 -0
- package/dist-server/server/box.js +345 -75
- package/dist-server/server/cli-phone-setup.js +2 -2
- package/dist-server/server/cli.js +84 -12
- package/dist-server/server/config.js +48 -18
- package/dist-server/server/container-computer.js +7 -0
- package/dist-server/server/contracts.js +5 -0
- package/dist-server/server/default-model-selection.js +1 -0
- package/dist-server/server/drivers/acp/core.js +112 -18
- package/dist-server/server/drivers/acp/opencode-go.js +6 -0
- package/dist-server/server/drivers/agents-proxy.js +76 -13
- package/dist-server/server/drivers/chat-mcp-tools.js +6 -1
- package/dist-server/server/drivers/claude.js +87 -63
- package/dist-server/server/drivers/codex.js +248 -51
- package/dist-server/server/drivers/native.js +6 -1
- package/dist-server/server/harness/bus.js +4 -0
- package/dist-server/server/harness/registry.js +3 -2
- package/dist-server/server/index.js +1007 -222
- package/dist-server/server/local-vm-lease.js +4 -0
- package/dist-server/server/managed-desktop.js +205 -0
- package/dist-server/server/mcp-http.js +337 -0
- package/dist-server/server/mcp-probe.js +67 -18
- package/dist-server/server/mcp-registry.js +153 -58
- package/dist-server/server/member-turn.js +13 -0
- package/dist-server/server/message-db.js +74 -8
- package/dist-server/server/model-context-window.js +20 -0
- package/dist-server/server/recall-disclosure.js +4 -0
- package/dist-server/server/recent-work.js +142 -0
- package/dist-server/server/redact.js +1 -0
- package/dist-server/server/request-auth.js +3 -1
- package/dist-server/server/room-handoffs.js +16 -6
- package/dist-server/server/routines.js +17 -0
- package/dist-server/server/screen-frame-gate.js +4 -94
- package/dist-server/server/sessions.js +22 -3
- package/dist-server/server/store.js +29 -2
- package/dist-server/server/surface.js +41 -19
- package/dist-server/server/team-setup-requests.js +1 -1
- package/dist-server/server/thread-log-rotation.js +104 -0
- package/dist-server/server/tts/fish.js +155 -0
- package/dist-server/server/tts/index.js +25 -3
- package/dist-server/server/vps-computer.js +26 -4
- package/dist-server/server/vps-ssh.js +88 -0
- package/dist-server/server/workspace-backup.js +11 -0
- package/dist-server/server/workspace.js +2 -1
- package/dist-server/shared/credential-request.js +10 -0
- package/dist-server/shared/tool-surface.js +62 -0
- package/dist-server/vps-container-mcp.js +41 -10
- package/package.json +1 -1
- package/dist/assets/index-D_TFZLlo.js +0 -305
- package/dist/assets/index-Pbb6Ao0s.css +0 -1
|
@@ -24,10 +24,25 @@ import { commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
|
24
24
|
import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
|
|
25
25
|
import { CodexDeviceAuthController } from "./codex-device-auth.js";
|
|
26
26
|
import { codexAccountEmail } from "./codex-identity.js";
|
|
27
|
+
import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
|
|
27
28
|
export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
28
29
|
const DRIVER_KIND = "codex";
|
|
29
30
|
const ASTRA_MODEL_ID = "gpt-6-astra";
|
|
30
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
|
+
}
|
|
31
46
|
/** Whether an installed Codex predates the release that exposes GPT-6 Astra
|
|
32
47
|
* through app-server. Unknown version formats stay quiet: a bad guess should
|
|
33
48
|
* never nag someone whose custom build may already support the model. */
|
|
@@ -79,10 +94,50 @@ function decodeConfig(raw) {
|
|
|
79
94
|
return {
|
|
80
95
|
cli: typeof o.cli === "string" ? o.cli : "codex",
|
|
81
96
|
fullAuto: o.fullAuto === true,
|
|
97
|
+
...(o.managed && typeof o.managed === "object" ? { managed: decodeManagedCodex(o.managed) } : {}),
|
|
82
98
|
};
|
|
83
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
|
+
// Plain HTTP leaks the Company API key; allow it only on loopback hosts,
|
|
107
|
+
// where a local proxy terminates TLS on the trusted machine instead.
|
|
108
|
+
const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
|
|
109
|
+
if (url.username || url.password || url.search || url.hash || (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)))
|
|
110
|
+
throw new Error("Invalid Company Codex endpoint.");
|
|
111
|
+
return { url: url.href.replace(/\/$/, ""), models: value.models };
|
|
112
|
+
}
|
|
113
|
+
export function managedCodexArgs(config) {
|
|
114
|
+
// Credential stays in the instance environment, never argv or config.toml.
|
|
115
|
+
// https://learn.chatgpt.com/docs/config-file/config-reference
|
|
116
|
+
return [
|
|
117
|
+
"-c", 'model_provider="openmaus_company"',
|
|
118
|
+
"-c", 'model_providers.openmaus_company.name="Company"',
|
|
119
|
+
"-c", `model_providers.openmaus_company.base_url=${JSON.stringify(config.url)}`,
|
|
120
|
+
"-c", 'model_providers.openmaus_company.env_key="OPENMAUSBOT_COMPANY_API_KEY"',
|
|
121
|
+
"-c", 'model_providers.openmaus_company.wire_api="responses"',
|
|
122
|
+
"-c", "model_providers.openmaus_company.requires_openai_auth=false",
|
|
123
|
+
"-c", 'cli_auth_credentials_store="ephemeral"',
|
|
124
|
+
"-c", "shell_environment_policy.ignore_default_excludes=false",
|
|
125
|
+
];
|
|
126
|
+
}
|
|
84
127
|
const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
|
|
85
128
|
const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
|
|
129
|
+
const skippedSseServers = new Set();
|
|
130
|
+
function noteSkippedSseServer(name) {
|
|
131
|
+
if (skippedSseServers.has(name))
|
|
132
|
+
return;
|
|
133
|
+
skippedSseServers.add(name);
|
|
134
|
+
console.error(`codex: MCP server ${JSON.stringify(name)} uses the SSE transport, which codex does not speak — it is available to Claude bots only`);
|
|
135
|
+
}
|
|
136
|
+
/** A TOML inline table for a `-c key=value` override; JSON string quoting
|
|
137
|
+
* is valid TOML basic-string quoting. */
|
|
138
|
+
function tomlInlineTable(entries) {
|
|
139
|
+
return `{ ${Object.entries(entries).map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`).join(", ")} }`;
|
|
140
|
+
}
|
|
86
141
|
/** RequestPermissionProfile uses null for permission families that were not
|
|
87
142
|
* requested; GrantedPermissionProfile requires those keys to be absent. */
|
|
88
143
|
function grantedPermissions(raw) {
|
|
@@ -370,12 +425,39 @@ export function codexNativeIncomingLogMessage(message, sensitiveResponseIds) {
|
|
|
370
425
|
return message;
|
|
371
426
|
}
|
|
372
427
|
function mountMcpServer(appServerArgs, env, name, server, preApproved = true) {
|
|
373
|
-
Object.assign(env, server.env);
|
|
374
428
|
const prefix = `mcp_servers.${name}`;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
429
|
+
if ("url" in server) {
|
|
430
|
+
// A remote server: codex connects itself. Header values are credentials
|
|
431
|
+
// (Authorization: Bearer …) and travel like env values — the child env
|
|
432
|
+
// holds them under harness-generated names, argv names only the variables.
|
|
433
|
+
// A bearer token goes through codex's own bearer setting, the path its
|
|
434
|
+
// remote servers are documented and exercised with; any other header
|
|
435
|
+
// rides env_http_headers.
|
|
436
|
+
appServerArgs.push("-c", `${prefix}.url=${JSON.stringify(server.url)}`);
|
|
437
|
+
const stem = `OMB_MCP_HEADER_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
438
|
+
const variables = {};
|
|
439
|
+
Object.entries(server.headers).forEach(([header, value], index) => {
|
|
440
|
+
const bearer = header.toLowerCase() === "authorization" ? /^Bearer\s+(\S+)$/i.exec(value) : null;
|
|
441
|
+
if (bearer) {
|
|
442
|
+
env[`${stem}_BEARER`] = bearer[1];
|
|
443
|
+
appServerArgs.push("-c", `${prefix}.bearer_token_env_var=${JSON.stringify(`${stem}_BEARER`)}`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const variable = `${stem}_${index}`;
|
|
447
|
+
env[variable] = value;
|
|
448
|
+
variables[header] = variable;
|
|
449
|
+
});
|
|
450
|
+
if (Object.keys(variables).length) {
|
|
451
|
+
appServerArgs.push("-c", `${prefix}.env_http_headers=${tomlInlineTable(variables)}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
Object.assign(env, server.env);
|
|
456
|
+
appServerArgs.push("-c", `${prefix}.command=${JSON.stringify(server.command)}`, "-c", `${prefix}.args=${JSON.stringify(server.args)}`,
|
|
457
|
+
// Values stay in the child environment; argv contains names only so
|
|
458
|
+
// credentials never appear in process listings or diagnostics.
|
|
459
|
+
"-c", `${prefix}.env_vars=${JSON.stringify(Object.keys(server.env))}`);
|
|
460
|
+
}
|
|
379
461
|
// Harness-owned servers are pre-quieted; a user-configured server keeps
|
|
380
462
|
// codex's on-request policy so its tool calls become approval cards.
|
|
381
463
|
if (preApproved) {
|
|
@@ -416,8 +498,10 @@ export const CodexDriver = {
|
|
|
416
498
|
return env;
|
|
417
499
|
};
|
|
418
500
|
const catalogEnv = childEnv();
|
|
419
|
-
let models = STATIC_CODEX_MODELS;
|
|
501
|
+
let models = config.managed ? { default: config.managed.models[0], options: config.managed.models.map(id => ({ id, label: id })) } : STATIC_CODEX_MODELS;
|
|
420
502
|
const refreshModels = async () => {
|
|
503
|
+
if (config.managed)
|
|
504
|
+
return;
|
|
421
505
|
try {
|
|
422
506
|
const resolved = await readCodexModelCatalog(catalogEnv, fetch, config.cli);
|
|
423
507
|
if (resolved.options.length)
|
|
@@ -447,9 +531,27 @@ export const CodexDriver = {
|
|
|
447
531
|
createdAt: new Date().toISOString(),
|
|
448
532
|
});
|
|
449
533
|
const sendTurn = async (turn) => {
|
|
534
|
+
if (config.managed) {
|
|
535
|
+
// One blanket refusal hides which prerequisite broke; name it so the
|
|
536
|
+
// person can fix the actual gap instead of reconnecting blind.
|
|
537
|
+
if (!turn.model) {
|
|
538
|
+
throw new Error("Company model access is unavailable: no model is selected. Reconnect your organization; personal billing will not be used.");
|
|
539
|
+
}
|
|
540
|
+
if (!config.managed.models.includes(turn.model)) {
|
|
541
|
+
throw new Error("Company model access is unavailable: " + turn.model + " is not approved for your organization. Reconnect your organization; personal billing will not be used.");
|
|
542
|
+
}
|
|
543
|
+
if (!input.environment.OPENMAUSBOT_COMPANY_API_KEY) {
|
|
544
|
+
throw new Error("Company model access is unavailable: OPENMAUSBOT_COMPANY_API_KEY is missing. Reconnect your organization; personal billing will not be used.");
|
|
545
|
+
}
|
|
546
|
+
if (!input.environment.CODEX_HOME) {
|
|
547
|
+
throw new Error("Company model access is unavailable: CODEX_HOME is missing. Reconnect your organization; personal billing will not be used.");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
450
550
|
// One driver instance serves many threads. Interrupt state belongs to
|
|
451
551
|
// this turn so activity elsewhere cannot cancel or revive its retry.
|
|
452
552
|
let stopRequested = false;
|
|
553
|
+
let promptSubmitted = false;
|
|
554
|
+
let recoveredMissingSession = false;
|
|
453
555
|
// Wakes a retry backoff the moment Stop arrives, so the turn settles
|
|
454
556
|
// now rather than after the full wait.
|
|
455
557
|
const stopSignal = new AbortController();
|
|
@@ -459,6 +561,8 @@ export const CodexDriver = {
|
|
|
459
561
|
// explicit mode, which takes precedence.
|
|
460
562
|
const approvalMode = turn.approvalMode ?? (config.fullAuto ? "full" : "ask");
|
|
461
563
|
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
564
|
+
if ("url" in server)
|
|
565
|
+
continue;
|
|
462
566
|
const reserved = Object.keys(server.env).find(isHarnessOwnedMcpEnvName);
|
|
463
567
|
if (reserved) {
|
|
464
568
|
throw new Error(`Custom MCP server “${name}” cannot set reserved environment variable “${reserved}”`);
|
|
@@ -473,7 +577,7 @@ export const CodexDriver = {
|
|
|
473
577
|
const retryScale = Number(process.env.FAKE_CODEX_RETRY_SCALE ?? "1");
|
|
474
578
|
const launchAttempt = async (attempt) => {
|
|
475
579
|
const env = childEnv();
|
|
476
|
-
const appServerArgs = ["app-server", ...codexLocalProviderArgs(env, turn.model)];
|
|
580
|
+
const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model))];
|
|
477
581
|
if (turn.integrations?.composio) {
|
|
478
582
|
mountMcpServer(appServerArgs, env, "openmausbot_connectors", turn.integrations.composio);
|
|
479
583
|
}
|
|
@@ -489,6 +593,13 @@ export const CodexDriver = {
|
|
|
489
593
|
mountMcpServer(appServerArgs, env, "browser", turn.integrations.browser);
|
|
490
594
|
}
|
|
491
595
|
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
596
|
+
// codex speaks streamable HTTP to a remote server, not the older
|
|
597
|
+
// SSE transport: such an entry still reaches Claude bots, and is
|
|
598
|
+
// left out here rather than mounted as something it is not
|
|
599
|
+
if ("url" in server && server.type === "sse") {
|
|
600
|
+
noteSkippedSseServer(name);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
492
603
|
mountMcpServer(appServerArgs, env, name, server, false);
|
|
493
604
|
}
|
|
494
605
|
if (turn.integrations?.phone) {
|
|
@@ -512,9 +623,12 @@ export const CodexDriver = {
|
|
|
512
623
|
lastError: "",
|
|
513
624
|
lastText: "",
|
|
514
625
|
sawStreamDelta: false,
|
|
515
|
-
// codex reports token usage as a running
|
|
516
|
-
// wants this turn's figure
|
|
626
|
+
// codex reports token usage as a running total for this app-server
|
|
627
|
+
// process. The harness wants this turn's figure: the total minus
|
|
628
|
+
// whatever the process already carried before turn/start (a resumed
|
|
629
|
+
// thread may restore earlier usage), banked on settle.
|
|
517
630
|
usage: undefined,
|
|
631
|
+
usageBaseline: undefined,
|
|
518
632
|
};
|
|
519
633
|
const asks = new Map();
|
|
520
634
|
let nextId = 1;
|
|
@@ -546,6 +660,18 @@ export const CodexDriver = {
|
|
|
546
660
|
rpcPending.set(id, {
|
|
547
661
|
resolve: (v) => {
|
|
548
662
|
clearTimeout(timer);
|
|
663
|
+
if (method === "thread/start" || method === "thread/resume") {
|
|
664
|
+
// Notifications can follow the thread response in the same
|
|
665
|
+
// stdout chunk, before the handshake await resumes.
|
|
666
|
+
const returnedId = v?.thread?.id;
|
|
667
|
+
const requestedId = method === "thread/resume"
|
|
668
|
+
&& params && typeof params === "object" && "threadId" in params
|
|
669
|
+
? params.threadId : null;
|
|
670
|
+
if (typeof returnedId === "string" && returnedId)
|
|
671
|
+
codexThreadId = returnedId;
|
|
672
|
+
else if (typeof requestedId === "string" && requestedId)
|
|
673
|
+
codexThreadId = requestedId;
|
|
674
|
+
}
|
|
549
675
|
if (method === "turn/start") {
|
|
550
676
|
if (typeof v?.turn?.id !== "string" || !v.turn.id) {
|
|
551
677
|
reject(new Error("Codex did not return a native turn id"));
|
|
@@ -641,6 +767,27 @@ export const CodexDriver = {
|
|
|
641
767
|
}
|
|
642
768
|
return;
|
|
643
769
|
}
|
|
770
|
+
// One ask card carries one question honestly: its choices would come
|
|
771
|
+
// from the first question alone and its one reply (including the
|
|
772
|
+
// timeout note) would be copied into every question id (#1237).
|
|
773
|
+
// Refuse the bundled call with a teaching error instead of
|
|
774
|
+
// fabricating per-question answers.
|
|
775
|
+
if (isQuestion && (!Array.isArray(params.questions) || params.questions.length !== 1)) {
|
|
776
|
+
const bundled = Array.isArray(params.questions) && params.questions.length > 1;
|
|
777
|
+
send({
|
|
778
|
+
jsonrpc: "2.0",
|
|
779
|
+
id: msg.id,
|
|
780
|
+
error: {
|
|
781
|
+
code: -32602,
|
|
782
|
+
message: bundled
|
|
783
|
+
? `ask supports one question per call; this request bundled ${params.questions.length}. Split it into separate asks, one question each.`
|
|
784
|
+
: Array.isArray(params.questions)
|
|
785
|
+
? "ask supports one question per call; this request sent none."
|
|
786
|
+
: "ask supports one question per call; params.questions must be an array with exactly one question.",
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
644
791
|
const mcpTool = isLegacyMcpPermission
|
|
645
792
|
? String(params.message ?? "").match(/tool "([^"]+)"/)?.[1]
|
|
646
793
|
: undefined;
|
|
@@ -730,6 +877,17 @@ export const CodexDriver = {
|
|
|
730
877
|
if (!connectionError) {
|
|
731
878
|
if (!codexThreadId || p.threadId !== codexThreadId)
|
|
732
879
|
return;
|
|
880
|
+
if (!codexTurnId && msg.method === "thread/tokenUsage/updated" && p.tokenUsage?.total) {
|
|
881
|
+
// A total reported before this turn exists is what the process
|
|
882
|
+
// carried in — a resumed thread restoring earlier usage. It is the
|
|
883
|
+
// baseline this turn's figure is measured from, never a reading to
|
|
884
|
+
// buffer and replay as if this turn produced it. (Codex names the
|
|
885
|
+
// turn before any model call, so a genuine first reading cannot
|
|
886
|
+
// land here.)
|
|
887
|
+
const t = p.tokenUsage.total;
|
|
888
|
+
state.usageBaseline = { input: t.inputTokens ?? 0, output: t.outputTokens ?? 0, cachedInput: t.cachedInputTokens ?? 0 };
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
733
891
|
if (!codexTurnId) {
|
|
734
892
|
// Some servers stream before acknowledging turn/start. Retain a
|
|
735
893
|
// bounded prefix, then filter against the authoritative response.
|
|
@@ -835,32 +993,44 @@ export const CodexDriver = {
|
|
|
835
993
|
break;
|
|
836
994
|
}
|
|
837
995
|
case "thread/tokenUsage/updated": {
|
|
838
|
-
// `
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
|
|
996
|
+
// `total` is everything this app-server process has used; `last`
|
|
997
|
+
// is the most recent model call. This turn's figure is the total
|
|
998
|
+
// minus what the process carried before turn/start went out (a
|
|
999
|
+
// resumed thread can restore earlier usage), so it never grows by
|
|
1000
|
+
// the whole thread per message and never counts only the final
|
|
1001
|
+
// call of a multi-step turn. codex's inputTokens already includes
|
|
1002
|
+
// cachedInputTokens; the cached share rides alongside so the UI
|
|
1003
|
+
// can say how much was context re-read rather than new text.
|
|
1004
|
+
const t = p.tokenUsage?.total;
|
|
1005
|
+
const last = p.tokenUsage?.last;
|
|
1006
|
+
const shape = (u) => ({
|
|
1007
|
+
input: u.inputTokens ?? 0, output: u.outputTokens ?? 0, cachedInput: u.cachedInputTokens ?? 0,
|
|
1008
|
+
});
|
|
1009
|
+
// (A total that arrived before this turn was named became the
|
|
1010
|
+
// baseline upstream and never reaches this switch.)
|
|
1011
|
+
if (t) {
|
|
1012
|
+
const b = state.usageBaseline ?? { input: 0, output: 0, cachedInput: 0 };
|
|
1013
|
+
const now = shape(t);
|
|
846
1014
|
state.usage = {
|
|
847
|
-
input:
|
|
848
|
-
output:
|
|
849
|
-
...(typeof
|
|
850
|
-
? { cachedInput: turnUsage.cachedInputTokens }
|
|
851
|
-
: {}),
|
|
1015
|
+
input: Math.max(0, now.input - b.input),
|
|
1016
|
+
output: Math.max(0, now.output - b.output),
|
|
1017
|
+
...(typeof t.cachedInputTokens === "number" ? { cachedInput: Math.max(0, now.cachedInput - b.cachedInput) } : {}),
|
|
852
1018
|
};
|
|
853
1019
|
}
|
|
854
|
-
|
|
1020
|
+
else if (last) {
|
|
1021
|
+
state.usage = { input: last.inputTokens ?? 0, output: last.outputTokens ?? 0, ...(typeof last.cachedInputTokens === "number" ? { cachedInput: last.cachedInputTokens } : {}) };
|
|
1022
|
+
}
|
|
855
1023
|
if (t) {
|
|
1024
|
+
const window = p.tokenUsage?.modelContextWindow;
|
|
856
1025
|
emit({
|
|
857
1026
|
...base(threadId, turnId),
|
|
858
1027
|
type: "thread.token-usage.updated",
|
|
859
1028
|
input: t.inputTokens ?? 0,
|
|
860
1029
|
output: t.outputTokens ?? 0,
|
|
861
|
-
...(typeof t.cachedInputTokens === "number"
|
|
862
|
-
|
|
863
|
-
|
|
1030
|
+
...(typeof t.cachedInputTokens === "number" ? { cachedInput: t.cachedInputTokens } : {}),
|
|
1031
|
+
// the last call's prompt is what fills the window
|
|
1032
|
+
...(last && typeof last.inputTokens === "number" ? { contextTokens: last.inputTokens } : {}),
|
|
1033
|
+
...(typeof window === "number" && window > 0 ? { contextWindow: window } : {}),
|
|
864
1034
|
});
|
|
865
1035
|
}
|
|
866
1036
|
break;
|
|
@@ -920,7 +1090,7 @@ export const CodexDriver = {
|
|
|
920
1090
|
if (pend) {
|
|
921
1091
|
rpcPending.delete(msg.id);
|
|
922
1092
|
if (msg.error)
|
|
923
|
-
pend.reject(new
|
|
1093
|
+
pend.reject(new CodexRpcError(msg.error));
|
|
924
1094
|
else
|
|
925
1095
|
pend.resolve(msg.result);
|
|
926
1096
|
}
|
|
@@ -971,7 +1141,16 @@ export const CodexDriver = {
|
|
|
971
1141
|
// websocket 426 logged at turn start.
|
|
972
1142
|
const recentStderr = stderrSinceOutput.trim();
|
|
973
1143
|
const hadStreamedOutput = codexTurnId !== null || state.sawStreamDelta;
|
|
974
|
-
|
|
1144
|
+
// A signal exit is terminal no matter what the stderr says:
|
|
1145
|
+
// something killed the process (OOM, kill -9), and classifyError
|
|
1146
|
+
// cannot see the signal — with code null, transient-looking recent
|
|
1147
|
+
// stderr could still mark a killed attempt retryable.
|
|
1148
|
+
// Classification also reads only stderr received after the last
|
|
1149
|
+
// protocol output; the lifetime buffer's tail can name a
|
|
1150
|
+
// long-past event (the websocket-426 misattribution).
|
|
1151
|
+
const verdict = signal !== null
|
|
1152
|
+
? { transient: false, reason: "interrupted" }
|
|
1153
|
+
: classifyError({ exitCode: code, stderr: recentStderr });
|
|
975
1154
|
// Safe re-dispatch: relaunch only when the app-server never
|
|
976
1155
|
// acknowledged turn/start — no native turn began, nothing was
|
|
977
1156
|
// streamed, so replaying the input cannot duplicate work. After
|
|
@@ -1069,35 +1248,48 @@ export const CodexDriver = {
|
|
|
1069
1248
|
// Removed bot rules are cleared without dropping native configured rules.
|
|
1070
1249
|
const cursor = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
1071
1250
|
let startedModel = null;
|
|
1251
|
+
let resumedNativeThread = false;
|
|
1252
|
+
let promptText = turn.text;
|
|
1072
1253
|
if (cursor) {
|
|
1254
|
+
const resumeThread = () => request("thread/resume", {
|
|
1255
|
+
threadId: cursor,
|
|
1256
|
+
developerInstructions,
|
|
1257
|
+
...approvalParams.thread,
|
|
1258
|
+
});
|
|
1073
1259
|
try {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1260
|
+
let resumed;
|
|
1261
|
+
try {
|
|
1262
|
+
resumed = await resumeThread();
|
|
1263
|
+
}
|
|
1264
|
+
catch (error) {
|
|
1265
|
+
if (!approvalParams.fallback || !permissionProfileUnsupported(error))
|
|
1266
|
+
throw error;
|
|
1267
|
+
// Older servers may require the legacy permission selector, but
|
|
1268
|
+
// still resume the same native thread before any user submission.
|
|
1269
|
+
approvalParams = approvalParams.fallback;
|
|
1270
|
+
resumed = await resumeThread();
|
|
1271
|
+
}
|
|
1079
1272
|
codexThreadId = resumed?.thread?.id ?? cursor;
|
|
1273
|
+
resumedNativeThread = true;
|
|
1080
1274
|
}
|
|
1081
1275
|
catch (error) {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
...approvalParams.thread,
|
|
1091
|
-
});
|
|
1092
|
-
codexThreadId = resumed?.thread?.id ?? cursor;
|
|
1093
|
-
}
|
|
1094
|
-
else {
|
|
1276
|
+
const failure = classifyResumeFailure({
|
|
1277
|
+
attempted: true,
|
|
1278
|
+
rejected: error instanceof CodexRpcError,
|
|
1279
|
+
promptSubmitted,
|
|
1280
|
+
producedOutput: state.sawStreamDelta,
|
|
1281
|
+
});
|
|
1282
|
+
if (!config.managed || recoveredMissingSession || stopRequested || state.settled ||
|
|
1283
|
+
!turn.recoveryText?.trim() || !missingNativeCodexThread(error, cursor) || !mayReplay(failure))
|
|
1095
1284
|
throw error;
|
|
1096
|
-
|
|
1285
|
+
// The prompt has never been submitted. Rebuild only missing Company
|
|
1286
|
+
// histories, once, through the same approved model/provider below.
|
|
1287
|
+
recoveredMissingSession = true;
|
|
1288
|
+
promptText = recoveryPromptFor({ recoveryText: turn.recoveryText, currentText: turn.text, failure }).text;
|
|
1097
1289
|
}
|
|
1098
1290
|
}
|
|
1099
1291
|
if (!codexThreadId) {
|
|
1100
|
-
const selection = decodeCodexSelection(turn.model);
|
|
1292
|
+
const selection = config.managed ? { model: turn.model, modelProvider: "openmaus_company" } : decodeCodexSelection(turn.model);
|
|
1101
1293
|
const startThread = () => request("thread/start", {
|
|
1102
1294
|
developerInstructions,
|
|
1103
1295
|
cwd: turn.cwd ?? homedir(),
|
|
@@ -1121,14 +1313,14 @@ export const CodexDriver = {
|
|
|
1121
1313
|
}
|
|
1122
1314
|
if (!codexThreadId)
|
|
1123
1315
|
throw new Error("Codex did not return a native thread id");
|
|
1124
|
-
await syncCodexInstructions(threadId, codexThreadId, developerInstructions,
|
|
1316
|
+
await syncCodexInstructions(threadId, codexThreadId, developerInstructions, resumedNativeThread, request);
|
|
1125
1317
|
emit({ ...base(threadId, turnId), type: "session.started", sessionId: codexThreadId, model: startedModel ?? turn.model ?? null });
|
|
1126
|
-
const promptText = turn.text;
|
|
1127
1318
|
const turnInput = [
|
|
1128
1319
|
...(promptText ? [{ type: "text", text: promptText }] : []),
|
|
1129
1320
|
...(turn.images ?? []).map((image) => ({ type: "localImage", path: image.path })),
|
|
1130
1321
|
];
|
|
1131
1322
|
const startTurn = () => {
|
|
1323
|
+
promptSubmitted = true;
|
|
1132
1324
|
startingNativeTurn = true;
|
|
1133
1325
|
return request("turn/start", {
|
|
1134
1326
|
threadId: codexThreadId,
|
|
@@ -1161,7 +1353,10 @@ export const CodexDriver = {
|
|
|
1161
1353
|
const message = e instanceof Error ? e.message : String(e);
|
|
1162
1354
|
const needsAuth = /(?:\b401\b|unauthorized|missing bearer|authentication required)/i.test(message);
|
|
1163
1355
|
const verdict = classifyError(failure);
|
|
1164
|
-
|
|
1356
|
+
// Three guards hold here: main's abandoned attempt never retries,
|
|
1357
|
+
// neither does a Company session already recovered once from canonical
|
|
1358
|
+
// history, and a Stop already asked for must not be undone by a relaunch.
|
|
1359
|
+
if (!state.settled && !abandoned && !recoveredMissingSession && !stopRequested && !needsAuth && verdict.transient && attempt < RETRY_MAX_ATTEMPTS - 1 && state.sawStreamDelta === false) {
|
|
1165
1360
|
const delayMs = computeBackoff(attempt);
|
|
1166
1361
|
attempt++;
|
|
1167
1362
|
emit({
|
|
@@ -1210,6 +1405,8 @@ export const CodexDriver = {
|
|
|
1210
1405
|
});
|
|
1211
1406
|
if (!version)
|
|
1212
1407
|
return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
|
|
1408
|
+
if (config.managed)
|
|
1409
|
+
return { state: "available", version, authenticated: Boolean(input.environment.OPENMAUSBOT_COMPANY_API_KEY && input.environment.CODEX_HOME), billing: "metered" };
|
|
1213
1410
|
const authenticated = await new Promise((resolve) => {
|
|
1214
1411
|
execCli(config.cli, ["login", "status"], { timeout: 8000, env }, (err, stdout, stderr) => resolve(!err && /^logged in\b/im.test(`${stdout}\n${stderr ?? ""}`)));
|
|
1215
1412
|
});
|
|
@@ -6,14 +6,19 @@ import { appendFileSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { NATIVE_DIR } from "../config.js";
|
|
8
8
|
import { redactSecrets } from "../redact.js";
|
|
9
|
+
import { capThreadLog, currentThreadLogCap } from "../thread-log-rotation.js";
|
|
9
10
|
export function appendNative(threadId, entry) {
|
|
11
|
+
const file = join(NATIVE_DIR, `${threadId}.ndjson`);
|
|
10
12
|
try {
|
|
11
13
|
// The session-setup messages carry the credentials the agent is handed —
|
|
12
14
|
// the box and comms tokens ride inside session/new's mcpServers env, and
|
|
13
15
|
// an MCP header can carry a Composio key. These files are ordinary
|
|
14
16
|
// 0644 files people paste into bug reports, so values are masked while
|
|
15
17
|
// the shape stays intact.
|
|
16
|
-
appendFileSync(
|
|
18
|
+
appendFileSync(file, JSON.stringify({ at: new Date().toISOString(), ...entry, msg: redactSecrets(entry.msg) }) + "\n", { mode: 0o600 });
|
|
19
|
+
// Best-effort size cap (#1280) — same rule as the write itself: never
|
|
20
|
+
// let logging break the run it is observing.
|
|
21
|
+
capThreadLog(file, currentThreadLogCap());
|
|
17
22
|
}
|
|
18
23
|
catch {
|
|
19
24
|
/* never let logging break a run */
|
|
@@ -8,6 +8,7 @@ import { appendFileSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { EVENTS_DIR } from "../config.js";
|
|
10
10
|
import { redactSecrets } from "../redact.js";
|
|
11
|
+
import { capThreadLog, currentThreadLogCap } from "../thread-log-rotation.js";
|
|
11
12
|
import { newId } from "../contracts.js";
|
|
12
13
|
const INCOMPLETE_LOG_MESSAGE = "Canonical event history is incomplete: OpenMausBot could not write one or more events to disk. Live updates will continue.";
|
|
13
14
|
export class EventBus {
|
|
@@ -43,6 +44,9 @@ export class EventBus {
|
|
|
43
44
|
this.appendLog(join(EVENTS_DIR, `${event.threadId}.ndjson`), persistedEvents.map((entry) => JSON.stringify(entry)).join("\n") + "\n", { mode: 0o600 });
|
|
44
45
|
if (pendingWarning)
|
|
45
46
|
this.pendingLogWarnings.delete(event.threadId);
|
|
47
|
+
// Best-effort size cap (#1280): an open thread's canonical log
|
|
48
|
+
// otherwise grows without bound for as long as the thread stays open.
|
|
49
|
+
capThreadLog(join(EVENTS_DIR, `${event.threadId}.ndjson`), currentThreadLogCap());
|
|
46
50
|
}
|
|
47
51
|
catch (error) {
|
|
48
52
|
// Never feed this warning back through publish(): that would retry the
|
|
@@ -46,7 +46,7 @@ export class ProviderRegistry {
|
|
|
46
46
|
this.npmPresent = options.npmAvailable ?? npmAvailable;
|
|
47
47
|
this.driversByKind = new Map(drivers.map((d) => [d.driverKind, d]));
|
|
48
48
|
}
|
|
49
|
-
async load(configs) {
|
|
49
|
+
async load(configs, decorate) {
|
|
50
50
|
for (const [instanceId, entry] of Object.entries(configs)) {
|
|
51
51
|
// Account edits replace only their own process/session state.
|
|
52
52
|
await this.dispose(instanceId);
|
|
@@ -80,7 +80,7 @@ export class ProviderRegistry {
|
|
|
80
80
|
enabled: entry.enabled ?? true,
|
|
81
81
|
config,
|
|
82
82
|
});
|
|
83
|
-
this.byId.set(instanceId, { instanceId, live });
|
|
83
|
+
this.byId.set(instanceId, { instanceId, live: decorate ? decorate(live) : live });
|
|
84
84
|
}
|
|
85
85
|
catch (e) {
|
|
86
86
|
this.byId.set(instanceId, {
|
|
@@ -225,6 +225,7 @@ export class ProviderRegistry {
|
|
|
225
225
|
browserMcp: inst.adapter.capabilities.browserMcp === true,
|
|
226
226
|
images: inst.adapter.capabilities.images === true,
|
|
227
227
|
effortLevels: inst.adapter.capabilities.effortLevels,
|
|
228
|
+
modelVariants: inst.adapter.capabilities.modelVariants === true,
|
|
228
229
|
queueing: inst.adapter.capabilities.queueing === true,
|
|
229
230
|
localComputerMcp: inst.adapter.capabilities.localComputerMcp === true,
|
|
230
231
|
approvalReview: inst.reviewPermission !== undefined,
|