@jungjaehoon/mama-os 0.22.1 → 0.24.0
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/CHANGELOG.md +22 -0
- package/README.md +3 -1
- package/dist/agent/agent-loop.d.ts +3 -9
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +419 -216
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/code-act/constants.d.ts +2 -2
- package/dist/agent/code-act/constants.d.ts.map +1 -1
- package/dist/agent/code-act/constants.js +16 -9
- package/dist/agent/code-act/constants.js.map +1 -1
- package/dist/agent/code-act/host-bridge.d.ts +12 -11
- package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
- package/dist/agent/code-act/host-bridge.js +92 -15
- package/dist/agent/code-act/host-bridge.js.map +1 -1
- package/dist/agent/code-act/index.d.ts +2 -0
- package/dist/agent/code-act/index.d.ts.map +1 -1
- package/dist/agent/code-act/index.js +5 -1
- package/dist/agent/code-act/index.js.map +1 -1
- package/dist/agent/code-act/tool-policy.d.ts +42 -0
- package/dist/agent/code-act/tool-policy.d.ts.map +1 -0
- package/dist/agent/code-act/tool-policy.js +122 -0
- package/dist/agent/code-act/tool-policy.js.map +1 -0
- package/dist/agent/code-act/type-definition-generator.d.ts +4 -3
- package/dist/agent/code-act/type-definition-generator.d.ts.map +1 -1
- package/dist/agent/code-act/type-definition-generator.js +26 -48
- package/dist/agent/code-act/type-definition-generator.js.map +1 -1
- package/dist/agent/codex-app-server-process.d.ts +116 -0
- package/dist/agent/codex-app-server-process.d.ts.map +1 -0
- package/dist/agent/codex-app-server-process.js +1382 -0
- package/dist/agent/codex-app-server-process.js.map +1 -0
- package/dist/agent/codex-home.d.ts +9 -0
- package/dist/agent/codex-home.d.ts.map +1 -1
- package/dist/agent/codex-home.js +626 -4
- package/dist/agent/codex-home.js.map +1 -1
- package/dist/agent/codex-thread-registry.d.ts +63 -0
- package/dist/agent/codex-thread-registry.d.ts.map +1 -0
- package/dist/agent/codex-thread-registry.js +433 -0
- package/dist/agent/codex-thread-registry.js.map +1 -0
- package/dist/agent/gateway-tool-executor.d.ts +20 -7
- package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
- package/dist/agent/gateway-tool-executor.js +254 -90
- package/dist/agent/gateway-tool-executor.js.map +1 -1
- package/dist/agent/gateway-tools.md +10 -5
- package/dist/agent/managed-agent-runtime-sync.d.ts.map +1 -1
- package/dist/agent/managed-agent-runtime-sync.js +0 -3
- package/dist/agent/managed-agent-runtime-sync.js.map +1 -1
- package/dist/agent/managed-agent-validation.js +1 -1
- package/dist/agent/managed-agent-validation.js.map +1 -1
- package/dist/agent/model-runner.d.ts +51 -3
- package/dist/agent/model-runner.d.ts.map +1 -1
- package/dist/agent/model-runner.js +1 -1
- package/dist/agent/model-runner.js.map +1 -1
- package/dist/agent/persistent-cli-adapter.d.ts +1 -0
- package/dist/agent/persistent-cli-adapter.d.ts.map +1 -1
- package/dist/agent/persistent-cli-adapter.js +3 -0
- package/dist/agent/persistent-cli-adapter.js.map +1 -1
- package/dist/agent/role-manager.d.ts +22 -1
- package/dist/agent/role-manager.d.ts.map +1 -1
- package/dist/agent/role-manager.js +39 -2
- package/dist/agent/role-manager.js.map +1 -1
- package/dist/agent/session-pool.d.ts +2 -2
- package/dist/agent/session-pool.d.ts.map +1 -1
- package/dist/agent/session-pool.js +6 -6
- package/dist/agent/session-pool.js.map +1 -1
- package/dist/agent/tool-registry.d.ts +14 -0
- package/dist/agent/tool-registry.d.ts.map +1 -1
- package/dist/agent/tool-registry.js +81 -12
- package/dist/agent/tool-registry.js.map +1 -1
- package/dist/agent/types.d.ts +49 -7
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/api/graph-api.d.ts.map +1 -1
- package/dist/api/graph-api.js +62 -40
- package/dist/api/graph-api.js.map +1 -1
- package/dist/api/operator-tasks-handler.d.ts.map +1 -1
- package/dist/api/operator-tasks-handler.js +6 -2
- package/dist/api/operator-tasks-handler.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +22 -14
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +7 -4
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +259 -23
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.js +2 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/config/config-manager.d.ts +0 -5
- package/dist/cli/config/config-manager.d.ts.map +1 -1
- package/dist/cli/config/config-manager.js +78 -6
- package/dist/cli/config/config-manager.js.map +1 -1
- package/dist/cli/config/types.d.ts +4 -4
- package/dist/cli/config/types.d.ts.map +1 -1
- package/dist/cli/config/types.js +36 -0
- package/dist/cli/config/types.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/runtime/agent-loop-init.d.ts +1 -1
- package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
- package/dist/cli/runtime/agent-loop-init.js +106 -60
- package/dist/cli/runtime/agent-loop-init.js.map +1 -1
- package/dist/cli/runtime/api-routes-init.d.ts +3 -0
- package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
- package/dist/cli/runtime/api-routes-init.js +226 -51
- package/dist/cli/runtime/api-routes-init.js.map +1 -1
- package/dist/cli/runtime/connector-init.d.ts.map +1 -1
- package/dist/cli/runtime/connector-init.js +13 -17
- package/dist/cli/runtime/connector-init.js.map +1 -1
- package/dist/cli/runtime/envelope-bootstrap.d.ts.map +1 -1
- package/dist/cli/runtime/envelope-bootstrap.js +13 -1
- package/dist/cli/runtime/envelope-bootstrap.js.map +1 -1
- package/dist/cli/runtime/gateway-init.d.ts +1 -1
- package/dist/cli/runtime/gateway-init.d.ts.map +1 -1
- package/dist/cli/runtime/gateway-init.js +5 -0
- package/dist/cli/runtime/gateway-init.js.map +1 -1
- package/dist/cli/runtime/mama-core-init.d.ts +2 -2
- package/dist/cli/runtime/mama-core-init.d.ts.map +1 -1
- package/dist/cli/runtime/mama-core-init.js +17 -0
- package/dist/cli/runtime/mama-core-init.js.map +1 -1
- package/dist/cli/runtime/memory-agent-init.d.ts +1 -1
- package/dist/cli/runtime/memory-agent-init.d.ts.map +1 -1
- package/dist/cli/runtime/memory-agent-init.js +16 -6
- package/dist/cli/runtime/memory-agent-init.js.map +1 -1
- package/dist/cli/runtime/types.d.ts +4 -26
- package/dist/cli/runtime/types.d.ts.map +1 -1
- package/dist/cli/runtime/utilities.js +2 -2
- package/dist/cli/runtime/utilities.js.map +1 -1
- package/dist/connectors/config-loader.d.ts +24 -0
- package/dist/connectors/config-loader.d.ts.map +1 -0
- package/dist/connectors/config-loader.js +205 -0
- package/dist/connectors/config-loader.js.map +1 -0
- package/dist/connectors/framework/types.d.ts +2 -0
- package/dist/connectors/framework/types.d.ts.map +1 -1
- package/dist/connectors/kagemusha/query-tools.js +2 -2
- package/dist/connectors/kagemusha/query-tools.js.map +1 -1
- package/dist/envelope/reactive-config.d.ts +2 -2
- package/dist/envelope/reactive-config.d.ts.map +1 -1
- package/dist/envelope/reactive-config.js +36 -4
- package/dist/envelope/reactive-config.js.map +1 -1
- package/dist/gateways/context-injector.d.ts +2 -1
- package/dist/gateways/context-injector.d.ts.map +1 -1
- package/dist/gateways/context-injector.js.map +1 -1
- package/dist/gateways/message-router.d.ts +19 -1
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +179 -24
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/gateways/telegram.d.ts.map +1 -1
- package/dist/gateways/telegram.js +13 -0
- package/dist/gateways/telegram.js.map +1 -1
- package/dist/gateways/types.d.ts +4 -1
- package/dist/gateways/types.d.ts.map +1 -1
- package/dist/memory/history-extractor.d.ts +0 -24
- package/dist/memory/history-extractor.d.ts.map +1 -1
- package/dist/memory/history-extractor.js +0 -132
- package/dist/memory/history-extractor.js.map +1 -1
- package/dist/memory/save-candidate-extractor.d.ts +3 -0
- package/dist/memory/save-candidate-extractor.d.ts.map +1 -1
- package/dist/memory/save-candidate-extractor.js +12 -1
- package/dist/memory/save-candidate-extractor.js.map +1 -1
- package/dist/memory/secret-filter.d.ts +22 -0
- package/dist/memory/secret-filter.d.ts.map +1 -0
- package/dist/memory/secret-filter.js +77 -0
- package/dist/memory/secret-filter.js.map +1 -0
- package/dist/multi-agent/agent-process-manager.d.ts +9 -1
- package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
- package/dist/multi-agent/agent-process-manager.js +197 -49
- package/dist/multi-agent/agent-process-manager.js.map +1 -1
- package/dist/multi-agent/multi-agent-base.d.ts.map +1 -1
- package/dist/multi-agent/multi-agent-base.js +2 -1
- package/dist/multi-agent/multi-agent-base.js.map +1 -1
- package/dist/multi-agent/runtime-process.d.ts +18 -15
- package/dist/multi-agent/runtime-process.d.ts.map +1 -1
- package/dist/multi-agent/runtime-process.js +63 -59
- package/dist/multi-agent/runtime-process.js.map +1 -1
- package/dist/multi-agent/types.d.ts +13 -7
- package/dist/multi-agent/types.d.ts.map +1 -1
- package/dist/multi-agent/types.js.map +1 -1
- package/dist/multi-agent/workflow-engine.js +3 -3
- package/dist/multi-agent/workflow-engine.js.map +1 -1
- package/dist/multi-agent/workflow-types.d.ts +2 -2
- package/dist/multi-agent/workflow-types.d.ts.map +1 -1
- package/dist/observability/code-audit.d.ts +4 -0
- package/dist/observability/code-audit.d.ts.map +1 -1
- package/dist/observability/code-audit.js +69 -0
- package/dist/observability/code-audit.js.map +1 -1
- package/dist/operator/briefs.d.ts +26 -0
- package/dist/operator/briefs.d.ts.map +1 -0
- package/dist/operator/briefs.js +128 -0
- package/dist/operator/briefs.js.map +1 -0
- package/dist/operator/operator-trigger-loop.d.ts +27 -0
- package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
- package/dist/operator/operator-trigger-loop.js +72 -12
- package/dist/operator/operator-trigger-loop.js.map +1 -1
- package/dist/operator/report-carry.d.ts +29 -0
- package/dist/operator/report-carry.d.ts.map +1 -0
- package/dist/operator/report-carry.js +81 -0
- package/dist/operator/report-carry.js.map +1 -0
- package/dist/operator/report-run.d.ts +4 -4
- package/dist/operator/shadow-capture.d.ts +20 -0
- package/dist/operator/shadow-capture.d.ts.map +1 -0
- package/dist/operator/shadow-capture.js +34 -0
- package/dist/operator/shadow-capture.js.map +1 -0
- package/dist/operator/situation-report.d.ts +9 -0
- package/dist/operator/situation-report.d.ts.map +1 -1
- package/dist/operator/situation-report.js +56 -26
- package/dist/operator/situation-report.js.map +1 -1
- package/dist/operator/task-ledger.d.ts +90 -2
- package/dist/operator/task-ledger.d.ts.map +1 -1
- package/dist/operator/task-ledger.js +359 -23
- package/dist/operator/task-ledger.js.map +1 -1
- package/dist/operator/trigger-author.d.ts +35 -1
- package/dist/operator/trigger-author.d.ts.map +1 -1
- package/dist/operator/trigger-author.js +73 -10
- package/dist/operator/trigger-author.js.map +1 -1
- package/dist/operator/worker-run.d.ts +89 -0
- package/dist/operator/worker-run.d.ts.map +1 -0
- package/dist/operator/worker-run.js +127 -0
- package/dist/operator/worker-run.js.map +1 -0
- package/dist/operator/workorder-consumer.d.ts +121 -0
- package/dist/operator/workorder-consumer.d.ts.map +1 -0
- package/dist/operator/workorder-consumer.js +253 -0
- package/dist/operator/workorder-consumer.js.map +1 -0
- package/dist/operator/workorder-hooks.d.ts +30 -0
- package/dist/operator/workorder-hooks.d.ts.map +1 -0
- package/dist/operator/workorder-hooks.js +70 -0
- package/dist/operator/workorder-hooks.js.map +1 -0
- package/dist/operator/workorder-publishers.d.ts +68 -0
- package/dist/operator/workorder-publishers.d.ts.map +1 -0
- package/dist/operator/workorder-publishers.js +150 -0
- package/dist/operator/workorder-publishers.js.map +1 -0
- package/dist/security/security-monitor.d.ts +7 -0
- package/dist/security/security-monitor.d.ts.map +1 -1
- package/dist/security/security-monitor.js +17 -0
- package/dist/security/security-monitor.js.map +1 -1
- package/dist/setup/setup-prompt.d.ts +1 -1
- package/dist/setup/setup-prompt.d.ts.map +1 -1
- package/dist/setup/setup-prompt.js +4 -5
- package/dist/setup/setup-prompt.js.map +1 -1
- package/dist/utils/untrusted-content.d.ts +13 -0
- package/dist/utils/untrusted-content.d.ts.map +1 -1
- package/dist/utils/untrusted-content.js +43 -0
- package/dist/utils/untrusted-content.js.map +1 -1
- package/package.json +2 -2
- package/public/viewer/js/modules/agents.js +13 -9
- package/public/viewer/js/modules/settings.js +15 -10
- package/public/viewer/src/modules/agents.ts +12 -10
- package/public/viewer/src/modules/settings.ts +17 -12
- package/public/viewer/src/utils/api.ts +2 -2
- package/templates/AGENTS.codex.md +2 -23
- package/templates/personas/conductor.md +1 -1
- package/dist/agent/codex-mcp-process.d.ts +0 -105
- package/dist/agent/codex-mcp-process.d.ts.map +0 -1
- package/dist/agent/codex-mcp-process.js +0 -748
- package/dist/agent/codex-mcp-process.js.map +0 -1
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodexAppServerProcess = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const node_readline_1 = require("node:readline");
|
|
10
|
+
const codex_home_js_1 = require("./codex-home.js");
|
|
11
|
+
const codex_thread_registry_js_1 = require("./codex-thread-registry.js");
|
|
12
|
+
const DEFAULT_TIMEOUT = 300_000;
|
|
13
|
+
const STOP_GRACE_MS = 200;
|
|
14
|
+
const STDERR_LIMIT = 4_000;
|
|
15
|
+
const CLIENT_INFO = { name: 'mama-codex-app-server', version: '1.0.0' };
|
|
16
|
+
const TURN_STATUSES = new Set(['completed', 'interrupted', 'failed', 'inProgress']);
|
|
17
|
+
const TURN_ITEM_VIEWS = new Set(['notLoaded', 'summary', 'full']);
|
|
18
|
+
const APPROVAL_REVIEWERS = new Set(['user', 'auto_review', 'guardian_subagent']);
|
|
19
|
+
const OVERLOADED_ERROR_CODE = -32001;
|
|
20
|
+
const OVERLOAD_RETRY_LIMIT = 4;
|
|
21
|
+
const OVERLOAD_RETRY_BASE_MS = 25;
|
|
22
|
+
const TURN_START_RECONCILE_GRACE_MS = 250;
|
|
23
|
+
class CodexAppServerRpcError extends Error {
|
|
24
|
+
code;
|
|
25
|
+
constructor(code, message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'CodexAppServerRpcError';
|
|
28
|
+
this.code = code;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function object(value) {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
33
|
+
? value
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
function stableJson(value) {
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return value.map(stableJson);
|
|
39
|
+
}
|
|
40
|
+
const record = object(value);
|
|
41
|
+
if (!record) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
return Object.fromEntries(Object.keys(record)
|
|
45
|
+
.sort()
|
|
46
|
+
.map((key) => [key, stableJson(record[key])]));
|
|
47
|
+
}
|
|
48
|
+
function deepFreeze(value) {
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
for (const item of value) {
|
|
51
|
+
deepFreeze(item);
|
|
52
|
+
}
|
|
53
|
+
Object.freeze(value);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const record = object(value);
|
|
57
|
+
if (record) {
|
|
58
|
+
for (const item of Object.values(record)) {
|
|
59
|
+
deepFreeze(item);
|
|
60
|
+
}
|
|
61
|
+
Object.freeze(record);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function snapshotHostToolBridge(bridge) {
|
|
65
|
+
if (!bridge) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const tools = bridge.tools
|
|
69
|
+
.map((tool) => stableJson(tool))
|
|
70
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
71
|
+
deepFreeze(tools);
|
|
72
|
+
const execute = bridge.execute.bind(bridge);
|
|
73
|
+
return Object.freeze({ tools, execute });
|
|
74
|
+
}
|
|
75
|
+
function errorMessage(value, fallback) {
|
|
76
|
+
const record = object(value);
|
|
77
|
+
return typeof record?.message === 'string' && record.message ? record.message : fallback;
|
|
78
|
+
}
|
|
79
|
+
function stringField(record, name, context) {
|
|
80
|
+
const value = record[name];
|
|
81
|
+
if (typeof value !== 'string') {
|
|
82
|
+
throw new Error(`Codex app-server returned malformed ${context}.${name}`);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function nullableStringField(record, name, context) {
|
|
87
|
+
const value = record[name];
|
|
88
|
+
if (value !== null && typeof value !== 'string') {
|
|
89
|
+
throw new Error(`Codex app-server returned malformed ${context}.${name}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function nullableNumberField(record, name, context) {
|
|
93
|
+
const value = record[name];
|
|
94
|
+
if (value !== null && (typeof value !== 'number' || !Number.isFinite(value))) {
|
|
95
|
+
throw new Error(`Codex app-server returned malformed ${context}.${name}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function validateTurn(value, context) {
|
|
99
|
+
const turn = object(value);
|
|
100
|
+
if (!turn) {
|
|
101
|
+
throw new Error(`Codex app-server returned malformed ${context}`);
|
|
102
|
+
}
|
|
103
|
+
stringField(turn, 'id', context);
|
|
104
|
+
if (!Array.isArray(turn.items) || !TURN_ITEM_VIEWS.has(String(turn.itemsView))) {
|
|
105
|
+
throw new Error(`Codex app-server returned malformed ${context} items`);
|
|
106
|
+
}
|
|
107
|
+
if (!TURN_STATUSES.has(String(turn.status))) {
|
|
108
|
+
throw new Error(`Codex app-server returned malformed ${context}.status`);
|
|
109
|
+
}
|
|
110
|
+
if (turn.error !== null) {
|
|
111
|
+
const error = object(turn.error);
|
|
112
|
+
if (!error ||
|
|
113
|
+
typeof error.message !== 'string' ||
|
|
114
|
+
error.codexErrorInfo === undefined ||
|
|
115
|
+
(error.additionalDetails !== null && typeof error.additionalDetails !== 'string')) {
|
|
116
|
+
throw new Error(`Codex app-server returned malformed ${context}.error`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
nullableNumberField(turn, 'startedAt', context);
|
|
120
|
+
nullableNumberField(turn, 'completedAt', context);
|
|
121
|
+
nullableNumberField(turn, 'durationMs', context);
|
|
122
|
+
return turn;
|
|
123
|
+
}
|
|
124
|
+
function validateThread(value) {
|
|
125
|
+
const thread = object(value);
|
|
126
|
+
if (!thread) {
|
|
127
|
+
throw new Error('Codex app-server returned malformed thread');
|
|
128
|
+
}
|
|
129
|
+
for (const field of ['id', 'sessionId', 'preview', 'modelProvider', 'cwd', 'cliVersion']) {
|
|
130
|
+
stringField(thread, field, 'thread');
|
|
131
|
+
}
|
|
132
|
+
for (const field of [
|
|
133
|
+
'forkedFromId',
|
|
134
|
+
'parentThreadId',
|
|
135
|
+
'path',
|
|
136
|
+
'threadSource',
|
|
137
|
+
'agentNickname',
|
|
138
|
+
'agentRole',
|
|
139
|
+
'name',
|
|
140
|
+
]) {
|
|
141
|
+
nullableStringField(thread, field, 'thread');
|
|
142
|
+
}
|
|
143
|
+
if (typeof thread.ephemeral !== 'boolean') {
|
|
144
|
+
throw new Error('Codex app-server returned malformed thread.ephemeral');
|
|
145
|
+
}
|
|
146
|
+
for (const field of ['createdAt', 'updatedAt']) {
|
|
147
|
+
if (typeof thread[field] !== 'number' || !Number.isFinite(thread[field])) {
|
|
148
|
+
throw new Error(`Codex app-server returned malformed thread.${field}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
nullableNumberField(thread, 'recencyAt', 'thread');
|
|
152
|
+
const status = object(thread.status);
|
|
153
|
+
if (!status ||
|
|
154
|
+
!['notLoaded', 'idle', 'systemError', 'active'].includes(String(status.type)) ||
|
|
155
|
+
(status.type === 'active' && !Array.isArray(status.activeFlags))) {
|
|
156
|
+
throw new Error('Codex app-server returned malformed thread.status');
|
|
157
|
+
}
|
|
158
|
+
const sourceValid = typeof thread.source === 'string' || object(thread.source) !== undefined;
|
|
159
|
+
if (!sourceValid ||
|
|
160
|
+
(thread.gitInfo !== null && !object(thread.gitInfo)) ||
|
|
161
|
+
!Array.isArray(thread.turns)) {
|
|
162
|
+
throw new Error('Codex app-server returned malformed thread metadata');
|
|
163
|
+
}
|
|
164
|
+
for (const turn of thread.turns) {
|
|
165
|
+
validateTurn(turn, 'thread.turns[]');
|
|
166
|
+
}
|
|
167
|
+
return thread;
|
|
168
|
+
}
|
|
169
|
+
function shaFile(path) {
|
|
170
|
+
return (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(path)).digest('hex');
|
|
171
|
+
}
|
|
172
|
+
function managedFileSignature(path) {
|
|
173
|
+
if (!(0, node_fs_1.existsSync)(path)) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const stat = (0, node_fs_1.statSync)(path);
|
|
177
|
+
return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
178
|
+
}
|
|
179
|
+
function ensurePrivateDirectory(path) {
|
|
180
|
+
(0, node_fs_1.mkdirSync)(path, { recursive: true, mode: 0o700 });
|
|
181
|
+
(0, node_fs_1.chmodSync)(path, 0o700);
|
|
182
|
+
}
|
|
183
|
+
function atomicPrivateWrite(path, value) {
|
|
184
|
+
ensurePrivateDirectory((0, node_path_1.dirname)(path));
|
|
185
|
+
const temporary = `${path}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
186
|
+
(0, node_fs_1.writeFileSync)(temporary, value, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
187
|
+
(0, node_fs_1.chmodSync)(temporary, 0o600);
|
|
188
|
+
(0, node_fs_1.renameSync)(temporary, path);
|
|
189
|
+
(0, node_fs_1.chmodSync)(path, 0o600);
|
|
190
|
+
}
|
|
191
|
+
function copyAuthAtomically(source, destination) {
|
|
192
|
+
if (!(0, node_fs_1.existsSync)(source) ||
|
|
193
|
+
(0, node_fs_1.statSync)(source).size === 0 ||
|
|
194
|
+
(0, node_path_1.resolve)(source) === (0, node_path_1.resolve)(destination)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
ensurePrivateDirectory((0, node_path_1.dirname)(destination));
|
|
198
|
+
const temporary = `${destination}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
199
|
+
(0, node_fs_1.copyFileSync)(source, temporary);
|
|
200
|
+
(0, node_fs_1.chmodSync)(temporary, 0o600);
|
|
201
|
+
(0, node_fs_1.renameSync)(temporary, destination);
|
|
202
|
+
(0, node_fs_1.chmodSync)(destination, 0o600);
|
|
203
|
+
}
|
|
204
|
+
function configuredSecretValues(launch) {
|
|
205
|
+
const names = new Set();
|
|
206
|
+
const addJsonString = (source) => {
|
|
207
|
+
try {
|
|
208
|
+
const name = JSON.parse(source);
|
|
209
|
+
if (typeof name === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
210
|
+
names.add(name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
throw new Error('Codex app-server launch config contained malformed environment quoting', {
|
|
215
|
+
cause: error,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
for (const argument of launch.args) {
|
|
220
|
+
for (const match of argument.matchAll(/env_vars\s*=\s*\[([^\]]*)\]/g)) {
|
|
221
|
+
for (const quoted of match[1].matchAll(/"(?:\\.|[^"\\])*"/g)) {
|
|
222
|
+
addJsonString(quoted[0]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const match of argument.matchAll(/bearer_token_env_var\s*=\s*("(?:\\.|[^"\\])*")/g)) {
|
|
226
|
+
addJsonString(match[1]);
|
|
227
|
+
}
|
|
228
|
+
for (const match of argument.matchAll(/env_http_headers\s*=\s*\{([^}]*)\}/g)) {
|
|
229
|
+
for (const binding of match[1].matchAll(/=\s*("(?:\\.|[^"\\])*")/g)) {
|
|
230
|
+
addJsonString(binding[1]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const values = new Set();
|
|
235
|
+
for (const name of names) {
|
|
236
|
+
const value = launch.env[name];
|
|
237
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
238
|
+
values.add(value);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return values;
|
|
242
|
+
}
|
|
243
|
+
class CodexAppServerProcess {
|
|
244
|
+
options;
|
|
245
|
+
registry;
|
|
246
|
+
child;
|
|
247
|
+
stdout;
|
|
248
|
+
stderr;
|
|
249
|
+
nextId = 0;
|
|
250
|
+
pending = new Map();
|
|
251
|
+
lateTurnStarts = new Map();
|
|
252
|
+
turnStartReconciliations = new Map();
|
|
253
|
+
turns = new Map();
|
|
254
|
+
sessions = new Map();
|
|
255
|
+
sessionQueues = new Map();
|
|
256
|
+
connectionQueue = Promise.resolve();
|
|
257
|
+
startPromise;
|
|
258
|
+
shutdownPromise;
|
|
259
|
+
stopped = false;
|
|
260
|
+
stderrTail = '';
|
|
261
|
+
secrets = new Set();
|
|
262
|
+
authFingerprint;
|
|
263
|
+
authFingerprintInitialized = false;
|
|
264
|
+
authSourceSignature;
|
|
265
|
+
managedConfigFingerprint;
|
|
266
|
+
managedConfigSignature;
|
|
267
|
+
secretLaunchFingerprint;
|
|
268
|
+
secretLaunchFingerprintInitialized = false;
|
|
269
|
+
killTimer;
|
|
270
|
+
finalKillTimer;
|
|
271
|
+
constructor(options) {
|
|
272
|
+
const mamaRoot = (0, node_path_1.join)((0, node_os_1.homedir)(), '.mama');
|
|
273
|
+
this.options = {
|
|
274
|
+
...options,
|
|
275
|
+
command: options.command ?? 'codex',
|
|
276
|
+
requestTimeout: options.requestTimeout ?? DEFAULT_TIMEOUT,
|
|
277
|
+
codexHome: (0, node_path_1.resolve)(options.codexHome ?? (0, node_path_1.join)(mamaRoot, '.codex')),
|
|
278
|
+
isolatedHome: (0, node_path_1.resolve)(options.isolatedHome ?? (0, node_path_1.join)(mamaRoot, 'codex-runtime', 'home')),
|
|
279
|
+
registryRoot: (0, node_path_1.resolve)(options.registryRoot ?? (0, node_path_1.join)(mamaRoot, 'codex-runtime', 'threads')),
|
|
280
|
+
cwd: (0, node_path_1.resolve)(options.cwd),
|
|
281
|
+
};
|
|
282
|
+
ensurePrivateDirectory((0, node_path_1.dirname)(this.options.registryRoot));
|
|
283
|
+
this.registry = new codex_thread_registry_js_1.CodexThreadRegistry({ rootDir: this.options.registryRoot });
|
|
284
|
+
}
|
|
285
|
+
async prompt(text, callbacks, overrides = {}) {
|
|
286
|
+
if (this.stopped) {
|
|
287
|
+
throw new Error('Codex app-server process is stopped');
|
|
288
|
+
}
|
|
289
|
+
const session = this.resolveSessionPolicy(overrides);
|
|
290
|
+
return this.enqueueSession(session.sessionKey, async () => {
|
|
291
|
+
try {
|
|
292
|
+
if (this.stopped) {
|
|
293
|
+
throw new Error('Codex app-server process is stopped');
|
|
294
|
+
}
|
|
295
|
+
const launch = (0, codex_home_js_1.buildCodexAppServerLaunchConfig)(this.options.mcpConfigPath, process.env);
|
|
296
|
+
if (overrides.resumeSession === false) {
|
|
297
|
+
this.registry.remove(session.sessionKey);
|
|
298
|
+
this.sessions.delete(session.sessionKey);
|
|
299
|
+
}
|
|
300
|
+
this.assertRegistryPolicy(session, launch);
|
|
301
|
+
await this.prepareConnection(launch, session.requestTimeout);
|
|
302
|
+
let state = this.sessions.get(session.sessionKey);
|
|
303
|
+
if (!state?.threadId) {
|
|
304
|
+
try {
|
|
305
|
+
state = await this.openThread(session, launch);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (this.child && !this.shutdownPromise) {
|
|
309
|
+
await this.shutdown(this.toError(error));
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
this.sessions.set(session.sessionKey, state);
|
|
314
|
+
}
|
|
315
|
+
const reconciliation = this.turnStartReconciliations.get(state.threadId);
|
|
316
|
+
if (reconciliation) {
|
|
317
|
+
await reconciliation.promise;
|
|
318
|
+
await this.prepareConnection(launch, session.requestTimeout);
|
|
319
|
+
state = this.sessions.get(session.sessionKey);
|
|
320
|
+
if (!state?.threadId) {
|
|
321
|
+
state = await this.openThread(session, launch);
|
|
322
|
+
this.sessions.set(session.sessionKey, state);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const turnText = state.bootstrapPending && session.systemPrompt
|
|
326
|
+
? `<system-reminder>\nFresh MAMA runtime context after resuming this durable thread:\n${session.systemPrompt.replace(/<\/system-reminder>/gi, '')}\n</system-reminder>\n\n${text}`
|
|
327
|
+
: text;
|
|
328
|
+
const result = await this.startTurn(state.threadId, turnText, callbacks, session.requestTimeout, session.hostToolBridge);
|
|
329
|
+
state.bootstrapPending = false;
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (error instanceof CodexAppServerRpcError &&
|
|
334
|
+
error.code !== OVERLOADED_ERROR_CODE &&
|
|
335
|
+
this.child &&
|
|
336
|
+
!this.shutdownPromise) {
|
|
337
|
+
await this.shutdown(error);
|
|
338
|
+
}
|
|
339
|
+
if (this.shutdownPromise) {
|
|
340
|
+
await this.shutdownPromise;
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
async reset(sessionKey = this.options.sessionKey) {
|
|
347
|
+
await this.enqueueSession(sessionKey, async () => {
|
|
348
|
+
this.registry.remove(sessionKey);
|
|
349
|
+
this.sessions.delete(sessionKey);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async stop() {
|
|
353
|
+
this.stopped = true;
|
|
354
|
+
await this.shutdown(new Error('Codex app-server process stopped'));
|
|
355
|
+
}
|
|
356
|
+
getThreadId(sessionKey = this.options.sessionKey) {
|
|
357
|
+
return this.sessions.get(sessionKey)?.threadId || undefined;
|
|
358
|
+
}
|
|
359
|
+
getStatus() {
|
|
360
|
+
return {
|
|
361
|
+
running: this.child !== undefined,
|
|
362
|
+
childPid: this.child?.pid,
|
|
363
|
+
pendingRequestCount: this.pending.size,
|
|
364
|
+
hasActiveTurn: this.turns.size > 0,
|
|
365
|
+
stdoutListenerCount: this.stdout?.listenerCount('line') ?? 0,
|
|
366
|
+
stderrListenerCount: this.stderr?.listenerCount('line') ?? 0,
|
|
367
|
+
shutdownTimerActive: this.killTimer !== undefined || this.finalKillTimer !== undefined,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
resolveSessionPolicy(overrides) {
|
|
371
|
+
const hostToolBridge = snapshotHostToolBridge(overrides.hostToolBridge);
|
|
372
|
+
return {
|
|
373
|
+
sessionKey: overrides.sessionKey ?? this.options.sessionKey,
|
|
374
|
+
model: overrides.model ?? this.options.model,
|
|
375
|
+
systemPrompt: overrides.systemPrompt ?? this.options.systemPrompt,
|
|
376
|
+
cwd: (0, node_path_1.resolve)(overrides.cwd ?? this.options.cwd),
|
|
377
|
+
sandbox: overrides.sandbox ?? this.options.sandbox,
|
|
378
|
+
requestTimeout: overrides.requestTimeout ?? this.options.requestTimeout,
|
|
379
|
+
policyFingerprint: overrides.policyFingerprint ?? this.options.policyFingerprint,
|
|
380
|
+
hostToolBridge,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
enqueueSession(sessionKey, operation) {
|
|
384
|
+
const previous = this.sessionQueues.get(sessionKey) ?? Promise.resolve();
|
|
385
|
+
const result = previous.catch(() => undefined).then(operation);
|
|
386
|
+
const tail = result.then(() => undefined, () => undefined);
|
|
387
|
+
this.sessionQueues.set(sessionKey, tail);
|
|
388
|
+
void tail.finally(() => {
|
|
389
|
+
if (this.sessionQueues.get(sessionKey) === tail) {
|
|
390
|
+
this.sessionQueues.delete(sessionKey);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
async prepareConnection(launch, requestTimeout) {
|
|
396
|
+
const operation = this.connectionQueue.then(async () => {
|
|
397
|
+
if (this.stopped) {
|
|
398
|
+
throw new Error('Codex app-server process is stopped');
|
|
399
|
+
}
|
|
400
|
+
if (this.shutdownPromise) {
|
|
401
|
+
await this.shutdownPromise;
|
|
402
|
+
}
|
|
403
|
+
if (this.stopped) {
|
|
404
|
+
throw new Error('Codex app-server process is stopped');
|
|
405
|
+
}
|
|
406
|
+
const refreshed = this.prepareManagedFiles(launch);
|
|
407
|
+
if (refreshed && this.child) {
|
|
408
|
+
await this.restart();
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
await this.ensureStarted(launch, requestTimeout);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
if (this.child && !this.shutdownPromise) {
|
|
415
|
+
await this.shutdown(this.toError(error));
|
|
416
|
+
}
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
this.connectionQueue = operation.catch(() => undefined);
|
|
421
|
+
await operation;
|
|
422
|
+
}
|
|
423
|
+
assertRegistryPolicy(session, launch) {
|
|
424
|
+
const record = this.registry.load(session.sessionKey);
|
|
425
|
+
if (!record) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const matches = record.model === session.model &&
|
|
429
|
+
record.cwd === session.cwd &&
|
|
430
|
+
record.systemPromptFingerprint === this.policyFingerprint(session) &&
|
|
431
|
+
record.mcpConfigFingerprint === launch.fingerprint;
|
|
432
|
+
if (!matches) {
|
|
433
|
+
throw new Error('Codex app-server thread policy mismatch; reset the session explicitly');
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
prepareManagedFiles(launch) {
|
|
437
|
+
ensurePrivateDirectory(this.options.codexHome);
|
|
438
|
+
ensurePrivateDirectory(this.options.isolatedHome);
|
|
439
|
+
const configPath = (0, node_path_1.join)(this.options.codexHome, 'config.toml');
|
|
440
|
+
const config = (0, codex_home_js_1.buildMAMACodexAppServerConfig)();
|
|
441
|
+
const configFingerprint = (0, codex_thread_registry_js_1.fingerprintText)(config);
|
|
442
|
+
const configSignature = managedFileSignature(configPath);
|
|
443
|
+
if (this.managedConfigFingerprint !== configFingerprint ||
|
|
444
|
+
this.managedConfigSignature !== configSignature) {
|
|
445
|
+
if (configSignature === undefined || shaFile(configPath) !== configFingerprint) {
|
|
446
|
+
atomicPrivateWrite(configPath, config);
|
|
447
|
+
}
|
|
448
|
+
this.managedConfigFingerprint = configFingerprint;
|
|
449
|
+
this.managedConfigSignature = managedFileSignature(configPath);
|
|
450
|
+
}
|
|
451
|
+
const sourceAuth = (0, node_path_1.join)((0, node_os_1.homedir)(), '.codex', 'auth.json');
|
|
452
|
+
const destinationAuth = (0, node_path_1.join)(this.options.codexHome, 'auth.json');
|
|
453
|
+
const sourceStat = (0, node_fs_1.existsSync)(sourceAuth) ? (0, node_fs_1.statSync)(sourceAuth) : undefined;
|
|
454
|
+
const sourceSignature = sourceStat && sourceStat.size > 0
|
|
455
|
+
? `${(0, node_fs_1.realpathSync)(sourceAuth)}:${sourceStat.size}:${sourceStat.mtimeMs}:${sourceStat.ctimeMs}`
|
|
456
|
+
: undefined;
|
|
457
|
+
let sourceFingerprint = this.authFingerprint;
|
|
458
|
+
if (!this.authFingerprintInitialized ||
|
|
459
|
+
this.authSourceSignature !== sourceSignature ||
|
|
460
|
+
(sourceSignature !== undefined && !(0, node_fs_1.existsSync)(destinationAuth))) {
|
|
461
|
+
sourceFingerprint = sourceSignature ? shaFile(sourceAuth) : undefined;
|
|
462
|
+
if (sourceFingerprint &&
|
|
463
|
+
(!(0, node_fs_1.existsSync)(destinationAuth) || shaFile(destinationAuth) !== sourceFingerprint)) {
|
|
464
|
+
copyAuthAtomically(sourceAuth, destinationAuth);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const changed = this.authFingerprintInitialized && this.authFingerprint !== sourceFingerprint;
|
|
468
|
+
this.authFingerprint = sourceFingerprint;
|
|
469
|
+
this.authFingerprintInitialized = true;
|
|
470
|
+
this.authSourceSignature = sourceSignature;
|
|
471
|
+
const secretChanged = this.secretLaunchFingerprintInitialized &&
|
|
472
|
+
this.secretLaunchFingerprint !== launch.secretFingerprint;
|
|
473
|
+
if (!this.secretLaunchFingerprintInitialized ||
|
|
474
|
+
this.secretLaunchFingerprint !== launch.secretFingerprint) {
|
|
475
|
+
this.secrets = configuredSecretValues(launch);
|
|
476
|
+
this.secretLaunchFingerprint = launch.secretFingerprint;
|
|
477
|
+
this.secretLaunchFingerprintInitialized = true;
|
|
478
|
+
}
|
|
479
|
+
return changed || secretChanged;
|
|
480
|
+
}
|
|
481
|
+
async ensureStarted(launch, requestTimeout) {
|
|
482
|
+
if (this.child) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!this.startPromise) {
|
|
486
|
+
this.startPromise = this.start(launch, requestTimeout);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
await this.startPromise;
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
this.startPromise = undefined;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async start(launch, requestTimeout) {
|
|
496
|
+
const child = (0, node_child_process_1.spawn)(this.options.command, ['app-server', '--strict-config', '--stdio', ...launch.args], {
|
|
497
|
+
cwd: this.options.cwd,
|
|
498
|
+
env: { ...launch.env, HOME: this.options.isolatedHome, CODEX_HOME: this.options.codexHome },
|
|
499
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
500
|
+
});
|
|
501
|
+
this.child = child;
|
|
502
|
+
this.stdout = (0, node_readline_1.createInterface)({ input: child.stdout, crlfDelay: Infinity });
|
|
503
|
+
this.stderr = (0, node_readline_1.createInterface)({ input: child.stderr, crlfDelay: Infinity });
|
|
504
|
+
this.stdout.on('line', (line) => this.handleLine(child, line));
|
|
505
|
+
this.stderr.on('line', (line) => this.handleStderr(line));
|
|
506
|
+
child.once('error', (error) => this.failProcess(child, error));
|
|
507
|
+
child.once('exit', (code, signal) => this.failProcess(child, new Error(`Codex app-server exited (${code ?? signal ?? 'unknown'})`)));
|
|
508
|
+
const initialized = await this.request('initialize', {
|
|
509
|
+
clientInfo: CLIENT_INFO,
|
|
510
|
+
capabilities: { experimentalApi: true },
|
|
511
|
+
}, requestTimeout);
|
|
512
|
+
const initializeResponse = object(initialized);
|
|
513
|
+
if (!initializeResponse ||
|
|
514
|
+
typeof initializeResponse.userAgent !== 'string' ||
|
|
515
|
+
typeof initializeResponse.codexHome !== 'string' ||
|
|
516
|
+
typeof initializeResponse.platformFamily !== 'string' ||
|
|
517
|
+
typeof initializeResponse.platformOs !== 'string') {
|
|
518
|
+
throw new Error('Codex app-server initialize returned a malformed response');
|
|
519
|
+
}
|
|
520
|
+
if ((0, node_fs_1.realpathSync)(initializeResponse.codexHome) !== (0, node_fs_1.realpathSync)(this.options.codexHome)) {
|
|
521
|
+
throw new Error('Codex app-server initialize returned an unexpected CODEX_HOME');
|
|
522
|
+
}
|
|
523
|
+
this.notify('initialized');
|
|
524
|
+
}
|
|
525
|
+
async openThread(session, launch) {
|
|
526
|
+
const record = this.registry.load(session.sessionKey);
|
|
527
|
+
if (record) {
|
|
528
|
+
const result = object(await this.request('thread/resume', {
|
|
529
|
+
threadId: record.threadId,
|
|
530
|
+
model: session.model,
|
|
531
|
+
cwd: session.cwd,
|
|
532
|
+
approvalPolicy: 'never',
|
|
533
|
+
sandbox: session.sandbox,
|
|
534
|
+
}, session.requestTimeout));
|
|
535
|
+
this.validateResponsePolicy(result, session);
|
|
536
|
+
this.validateInstructionMetadata(result, session);
|
|
537
|
+
const resumed = validateThread(result?.thread);
|
|
538
|
+
if (typeof resumed?.id !== 'string' || resumed.id !== record.threadId) {
|
|
539
|
+
throw new Error('Codex app-server resumed an unexpected thread');
|
|
540
|
+
}
|
|
541
|
+
return { threadId: record.threadId, bootstrapPending: true };
|
|
542
|
+
}
|
|
543
|
+
const threadStartParams = {
|
|
544
|
+
model: session.model,
|
|
545
|
+
cwd: session.cwd,
|
|
546
|
+
approvalPolicy: 'never',
|
|
547
|
+
sandbox: session.sandbox,
|
|
548
|
+
baseInstructions: session.systemPrompt,
|
|
549
|
+
config: {},
|
|
550
|
+
};
|
|
551
|
+
if (session.hostToolBridge) {
|
|
552
|
+
threadStartParams.dynamicTools = session.hostToolBridge.tools;
|
|
553
|
+
}
|
|
554
|
+
const result = object(await this.request('thread/start', threadStartParams, session.requestTimeout));
|
|
555
|
+
this.validateResponsePolicy(result, session);
|
|
556
|
+
this.validateInstructionMetadata(result, session);
|
|
557
|
+
const thread = validateThread(result?.thread);
|
|
558
|
+
if (typeof thread?.id !== 'string' || !thread.id) {
|
|
559
|
+
throw new Error('Codex app-server thread/start returned no thread id');
|
|
560
|
+
}
|
|
561
|
+
this.registry.save({
|
|
562
|
+
sessionKey: session.sessionKey,
|
|
563
|
+
threadId: thread.id,
|
|
564
|
+
model: session.model,
|
|
565
|
+
cwd: session.cwd,
|
|
566
|
+
systemPromptFingerprint: this.policyFingerprint(session),
|
|
567
|
+
mcpConfigFingerprint: launch.fingerprint,
|
|
568
|
+
});
|
|
569
|
+
return { threadId: thread.id, bootstrapPending: false };
|
|
570
|
+
}
|
|
571
|
+
policyFingerprint(session) {
|
|
572
|
+
const base = session.policyFingerprint ?? (0, codex_thread_registry_js_1.fingerprintText)(session.systemPrompt);
|
|
573
|
+
const tools = session.hostToolBridge?.tools;
|
|
574
|
+
if (!tools?.length) {
|
|
575
|
+
return base;
|
|
576
|
+
}
|
|
577
|
+
return (0, codex_thread_registry_js_1.fingerprintText)(`${base}\n${JSON.stringify(tools)}`);
|
|
578
|
+
}
|
|
579
|
+
validateInstructionMetadata(result, session) {
|
|
580
|
+
const sources = result?.instructionSources;
|
|
581
|
+
if (!Array.isArray(sources)) {
|
|
582
|
+
throw new Error('Codex app-server returned malformed instruction sources');
|
|
583
|
+
}
|
|
584
|
+
const managedRoots = [session.cwd, this.options.codexHome].map((root) => (0, node_fs_1.realpathSync)(root));
|
|
585
|
+
for (const source of sources) {
|
|
586
|
+
if (typeof source !== 'string') {
|
|
587
|
+
throw new Error('Codex app-server returned malformed instruction source');
|
|
588
|
+
}
|
|
589
|
+
let path;
|
|
590
|
+
try {
|
|
591
|
+
path = (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(session.cwd, source));
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
throw new Error('Codex app-server loaded an instruction source outside managed roots', {
|
|
595
|
+
cause: error,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
const allowed = managedRoots.some((root) => {
|
|
599
|
+
const childPath = (0, node_path_1.relative)(root, path);
|
|
600
|
+
return (childPath === '' ||
|
|
601
|
+
(childPath !== '..' && !childPath.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(childPath)));
|
|
602
|
+
});
|
|
603
|
+
if (!allowed) {
|
|
604
|
+
throw new Error('Codex app-server loaded an instruction source outside managed roots');
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
validateResponsePolicy(result, session) {
|
|
609
|
+
if (!result) {
|
|
610
|
+
throw new Error('Codex app-server returned a malformed thread response');
|
|
611
|
+
}
|
|
612
|
+
if (typeof result.modelProvider !== 'string' ||
|
|
613
|
+
(result.serviceTier !== null && typeof result.serviceTier !== 'string') ||
|
|
614
|
+
!APPROVAL_REVIEWERS.has(String(result.approvalsReviewer)) ||
|
|
615
|
+
(result.reasoningEffort !== null && typeof result.reasoningEffort !== 'string')) {
|
|
616
|
+
throw new Error('Codex app-server returned malformed thread response metadata');
|
|
617
|
+
}
|
|
618
|
+
if (typeof result.model !== 'string' || result.model !== session.model) {
|
|
619
|
+
throw new Error('Codex app-server response model did not match the requested policy');
|
|
620
|
+
}
|
|
621
|
+
if (typeof result.cwd !== 'string' || (0, node_path_1.resolve)(result.cwd) !== session.cwd) {
|
|
622
|
+
throw new Error('Codex app-server response cwd did not match the requested policy');
|
|
623
|
+
}
|
|
624
|
+
if (result.approvalPolicy !== 'never') {
|
|
625
|
+
throw new Error('Codex app-server response approval policy was not never');
|
|
626
|
+
}
|
|
627
|
+
const sandbox = object(result.sandbox);
|
|
628
|
+
const expectedSandboxType = {
|
|
629
|
+
'read-only': 'readOnly',
|
|
630
|
+
'workspace-write': 'workspaceWrite',
|
|
631
|
+
'danger-full-access': 'dangerFullAccess',
|
|
632
|
+
}[session.sandbox];
|
|
633
|
+
if (typeof sandbox?.type !== 'string' || sandbox.type !== expectedSandboxType) {
|
|
634
|
+
throw new Error('Codex app-server response sandbox did not match the requested policy');
|
|
635
|
+
}
|
|
636
|
+
if (sandbox.type === 'readOnly' && typeof sandbox.networkAccess !== 'boolean') {
|
|
637
|
+
throw new Error('Codex app-server returned a malformed read-only sandbox policy');
|
|
638
|
+
}
|
|
639
|
+
if (sandbox.type === 'workspaceWrite' &&
|
|
640
|
+
(!Array.isArray(sandbox.writableRoots) ||
|
|
641
|
+
typeof sandbox.networkAccess !== 'boolean' ||
|
|
642
|
+
typeof sandbox.excludeTmpdirEnvVar !== 'boolean' ||
|
|
643
|
+
typeof sandbox.excludeSlashTmp !== 'boolean')) {
|
|
644
|
+
throw new Error('Codex app-server returned a malformed workspace-write sandbox policy');
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
startTurn(threadId, text, callbacks, requestTimeout, hostToolBridge) {
|
|
648
|
+
return new Promise((resolveTurn, rejectTurn) => {
|
|
649
|
+
const abortController = new AbortController();
|
|
650
|
+
const timer = setTimeout(() => {
|
|
651
|
+
const error = new Error(`Codex app-server turn timed out after ${requestTimeout}ms`);
|
|
652
|
+
this.timeoutTurn(threadId, error, requestTimeout);
|
|
653
|
+
}, requestTimeout);
|
|
654
|
+
timer.unref();
|
|
655
|
+
const pendingTurn = {
|
|
656
|
+
threadId,
|
|
657
|
+
chunks: [],
|
|
658
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
659
|
+
timer,
|
|
660
|
+
queuedNotifications: [],
|
|
661
|
+
queuedToolRequests: [],
|
|
662
|
+
toolCallQueue: Promise.resolve(),
|
|
663
|
+
toolCallResults: new Map(),
|
|
664
|
+
stoppingCallIds: new Set(),
|
|
665
|
+
hostToolBridge,
|
|
666
|
+
abortController,
|
|
667
|
+
intentionalStop: false,
|
|
668
|
+
onDelta: callbacks?.onDelta,
|
|
669
|
+
resolve: resolveTurn,
|
|
670
|
+
reject: rejectTurn,
|
|
671
|
+
};
|
|
672
|
+
this.turns.set(threadId, pendingTurn);
|
|
673
|
+
this.request('turn/start', {
|
|
674
|
+
threadId,
|
|
675
|
+
input: [{ type: 'text', text, text_elements: [] }],
|
|
676
|
+
}, requestTimeout)
|
|
677
|
+
.then((value) => {
|
|
678
|
+
const turn = validateTurn(object(value)?.turn, 'turn/start turn');
|
|
679
|
+
if (typeof turn?.id !== 'string' || !turn.id) {
|
|
680
|
+
this.failTurn(threadId, new Error('Codex app-server turn/start returned no turn id'));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const activeTurn = this.turns.get(threadId);
|
|
684
|
+
if (activeTurn === pendingTurn) {
|
|
685
|
+
activeTurn.turnId = turn.id;
|
|
686
|
+
const queued = activeTurn.queuedNotifications.splice(0);
|
|
687
|
+
for (const notification of queued) {
|
|
688
|
+
this.handleNotification(notification.method, notification.params);
|
|
689
|
+
}
|
|
690
|
+
const queuedToolRequests = activeTurn.queuedToolRequests.splice(0);
|
|
691
|
+
for (const request of queuedToolRequests) {
|
|
692
|
+
this.handleDynamicToolRequest(request, activeTurn);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
else if (this.turnStartReconciliations.has(threadId)) {
|
|
696
|
+
void this.reconcileAcknowledgedTurn(threadId, turn.id, requestTimeout);
|
|
697
|
+
}
|
|
698
|
+
})
|
|
699
|
+
.catch((error) => {
|
|
700
|
+
if (this.turns.get(threadId) === pendingTurn) {
|
|
701
|
+
this.failTurn(threadId, this.toError(error));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const reconciliation = this.turnStartReconciliations.get(threadId);
|
|
705
|
+
if (!reconciliation) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (error instanceof CodexAppServerRpcError) {
|
|
709
|
+
this.completeTurnStartReconciliation(threadId, reconciliation);
|
|
710
|
+
}
|
|
711
|
+
else if (!reconciliation.recoveryTimer) {
|
|
712
|
+
void this.shutdown(this.toError(error)).finally(() => {
|
|
713
|
+
this.completeTurnStartReconciliation(threadId, reconciliation);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
async request(method, params, requestTimeout = this.options.requestTimeout) {
|
|
720
|
+
for (let attempt = 0;; attempt++) {
|
|
721
|
+
try {
|
|
722
|
+
return await this.requestOnce(method, params, requestTimeout);
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
if (!(error instanceof CodexAppServerRpcError) ||
|
|
726
|
+
error.code !== OVERLOADED_ERROR_CODE ||
|
|
727
|
+
attempt >= OVERLOAD_RETRY_LIMIT - 1) {
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
const delay = OVERLOAD_RETRY_BASE_MS * 2 ** attempt + Math.floor(Math.random() * 10);
|
|
731
|
+
await new Promise((resolveDelay) => {
|
|
732
|
+
const timer = setTimeout(resolveDelay, delay);
|
|
733
|
+
timer.unref();
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
requestOnce(method, params, requestTimeout) {
|
|
739
|
+
const child = this.child;
|
|
740
|
+
if (!child?.stdin.writable) {
|
|
741
|
+
return Promise.reject(new Error('Codex app-server stdin is not writable'));
|
|
742
|
+
}
|
|
743
|
+
const id = ++this.nextId;
|
|
744
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
745
|
+
const timer = setTimeout(() => {
|
|
746
|
+
this.pending.delete(id);
|
|
747
|
+
const error = new Error(`Codex app-server ${method} timed out after ${requestTimeout}ms`);
|
|
748
|
+
const threadId = method === 'turn/start' ? object(params)?.threadId : undefined;
|
|
749
|
+
if (typeof threadId === 'string' && threadId) {
|
|
750
|
+
const reconciliation = this.getOrCreateTurnStartReconciliation(threadId);
|
|
751
|
+
reconciliation.recoveryTimer = setTimeout(() => {
|
|
752
|
+
this.lateTurnStarts.delete(id);
|
|
753
|
+
void this.shutdown(error).finally(() => {
|
|
754
|
+
this.completeTurnStartReconciliation(threadId, reconciliation);
|
|
755
|
+
});
|
|
756
|
+
}, Math.max(requestTimeout, TURN_START_RECONCILE_GRACE_MS));
|
|
757
|
+
reconciliation.recoveryTimer.unref();
|
|
758
|
+
this.lateTurnStarts.set(id, { threadId, requestTimeout, reconciliation });
|
|
759
|
+
rejectRequest(error);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
rejectRequest(error);
|
|
763
|
+
void this.shutdown(error);
|
|
764
|
+
}, requestTimeout);
|
|
765
|
+
timer.unref();
|
|
766
|
+
this.pending.set(id, { method, timer, resolve: resolveRequest, reject: rejectRequest });
|
|
767
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
notify(method, params) {
|
|
771
|
+
if (!this.child?.stdin.writable) {
|
|
772
|
+
throw new Error('Codex app-server stdin is not writable');
|
|
773
|
+
}
|
|
774
|
+
const message = params === undefined ? { jsonrpc: '2.0', method } : { jsonrpc: '2.0', method, params };
|
|
775
|
+
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
776
|
+
}
|
|
777
|
+
handleLine(child, line) {
|
|
778
|
+
if (child !== this.child || !line.trim()) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
let message;
|
|
782
|
+
try {
|
|
783
|
+
const parsed = JSON.parse(line);
|
|
784
|
+
const record = object(parsed);
|
|
785
|
+
if (!record) {
|
|
786
|
+
throw new Error('not an object');
|
|
787
|
+
}
|
|
788
|
+
message = record;
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
const error = new Error('Codex app-server emitted malformed JSON');
|
|
792
|
+
this.failAll(error);
|
|
793
|
+
void this.shutdown(error);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
// Codex 0.144 accepts JSON-RPC 2.0 requests but omits the `jsonrpc`
|
|
797
|
+
// member from its responses and notifications on the stdio wire.
|
|
798
|
+
if (message.jsonrpc !== undefined && message.jsonrpc !== '2.0') {
|
|
799
|
+
const error = new Error('Codex app-server emitted a malformed protocol message');
|
|
800
|
+
this.failAll(error);
|
|
801
|
+
void this.shutdown(error);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const hasMethod = typeof message.method === 'string';
|
|
805
|
+
const hasResult = Object.hasOwn(message, 'result');
|
|
806
|
+
const hasError = Object.hasOwn(message, 'error');
|
|
807
|
+
const hasRequestId = typeof message.id === 'number' || typeof message.id === 'string';
|
|
808
|
+
const serverRequest = hasMethod && hasRequestId && !hasResult && !hasError;
|
|
809
|
+
const notification = hasMethod && !hasRequestId && !hasResult && !hasError;
|
|
810
|
+
const response = !hasMethod && typeof message.id === 'number' && hasResult !== hasError;
|
|
811
|
+
if ([serverRequest, notification, response].filter(Boolean).length !== 1) {
|
|
812
|
+
const error = new Error('Codex app-server emitted a malformed protocol message');
|
|
813
|
+
this.failAll(error);
|
|
814
|
+
void this.shutdown(error);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (typeof message.method === 'string' &&
|
|
818
|
+
(typeof message.id === 'number' || typeof message.id === 'string')) {
|
|
819
|
+
this.handleServerRequest(child, message.id, message.method, message.params);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (typeof message.id === 'number') {
|
|
823
|
+
const lateTurnStart = this.lateTurnStarts.get(message.id);
|
|
824
|
+
if (lateTurnStart) {
|
|
825
|
+
this.handleLateTurnStartResponse(message.id, lateTurnStart, message);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const pending = this.pending.get(message.id);
|
|
829
|
+
if (!pending) {
|
|
830
|
+
const error = new Error('Codex app-server response id did not match a pending request');
|
|
831
|
+
this.failAll(error);
|
|
832
|
+
void this.shutdown(error);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
clearTimeout(pending.timer);
|
|
836
|
+
this.pending.delete(message.id);
|
|
837
|
+
if (message.error !== undefined) {
|
|
838
|
+
const rpcError = object(message.error);
|
|
839
|
+
const code = typeof rpcError?.code === 'number' ? rpcError.code : 0;
|
|
840
|
+
pending.reject(new CodexAppServerRpcError(code, this.redact(errorMessage(message.error, `${pending.method} failed`))));
|
|
841
|
+
}
|
|
842
|
+
else if (!Object.hasOwn(message, 'result')) {
|
|
843
|
+
pending.reject(new Error(`Codex app-server ${pending.method} returned a malformed response`));
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
pending.resolve(message.result);
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (typeof message.method === 'string') {
|
|
851
|
+
this.handleNotification(message.method, message.params);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
handleNotification(method, params) {
|
|
855
|
+
const data = object(params);
|
|
856
|
+
if (!data || typeof data.threadId !== 'string') {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const turn = this.turns.get(data.threadId);
|
|
860
|
+
if (!turn) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const completedTurn = method === 'turn/completed' ? object(data.turn) : undefined;
|
|
864
|
+
const eventTurnId = typeof data.turnId === 'string'
|
|
865
|
+
? data.turnId
|
|
866
|
+
: typeof completedTurn?.id === 'string'
|
|
867
|
+
? completedTurn.id
|
|
868
|
+
: undefined;
|
|
869
|
+
if (eventTurnId && !turn.turnId) {
|
|
870
|
+
turn.queuedNotifications.push({ method, params });
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (eventTurnId && eventTurnId !== turn.turnId) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (method === 'item/agentMessage/delta') {
|
|
877
|
+
if (typeof data.turnId !== 'string' || data.turnId !== turn.turnId) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (typeof data.delta === 'string') {
|
|
881
|
+
turn.chunks.push(data.delta);
|
|
882
|
+
try {
|
|
883
|
+
turn.onDelta?.(data.delta);
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
this.failTurn(turn.threadId, this.toError(error));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (method === 'thread/tokenUsage/updated') {
|
|
892
|
+
if (typeof data.turnId !== 'string' || data.turnId !== turn.turnId) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const last = object(object(data.tokenUsage)?.last);
|
|
896
|
+
turn.usage = {
|
|
897
|
+
input_tokens: typeof last?.inputTokens === 'number' ? last.inputTokens : 0,
|
|
898
|
+
output_tokens: typeof last?.outputTokens === 'number' ? last.outputTokens : 0,
|
|
899
|
+
cache_read_input_tokens: typeof last?.cachedInputTokens === 'number' ? last.cachedInputTokens : undefined,
|
|
900
|
+
};
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (method !== 'turn/completed') {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const completed = completedTurn;
|
|
907
|
+
if (!completed ||
|
|
908
|
+
typeof completed.id !== 'string' ||
|
|
909
|
+
(turn.turnId && completed.id !== turn.turnId))
|
|
910
|
+
return;
|
|
911
|
+
const status = completed.status;
|
|
912
|
+
if (status === 'inProgress') {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
validateTurn(completed, 'turn/completed turn');
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
this.failTurn(turn.threadId, this.toError(error));
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (turn.abortError) {
|
|
923
|
+
this.failTurn(turn.threadId, turn.abortError);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (status === 'failed') {
|
|
927
|
+
this.failTurn(turn.threadId, new Error(this.redact(errorMessage(completed.error, 'Codex app-server turn failed'))));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (status === 'interrupted') {
|
|
931
|
+
if (turn.abortError) {
|
|
932
|
+
this.failTurn(turn.threadId, turn.abortError);
|
|
933
|
+
}
|
|
934
|
+
else if (turn.intentionalStop) {
|
|
935
|
+
this.resolveTurn(turn);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
this.failTurn(turn.threadId, new Error('Codex app-server turn was interrupted'));
|
|
939
|
+
}
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (status !== 'completed') {
|
|
943
|
+
this.failTurn(turn.threadId, new Error(`Codex app-server returned unknown turn status: ${String(status)}`));
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
this.resolveTurn(turn);
|
|
947
|
+
}
|
|
948
|
+
resolveTurn(turn) {
|
|
949
|
+
if (this.turns.get(turn.threadId) !== turn) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
this.turns.delete(turn.threadId);
|
|
953
|
+
clearTimeout(turn.timer);
|
|
954
|
+
this.clearTurnCallbacks(turn);
|
|
955
|
+
turn.resolve({
|
|
956
|
+
response: turn.chunks.join(''),
|
|
957
|
+
usage: turn.usage,
|
|
958
|
+
session_id: turn.threadId,
|
|
959
|
+
toolUseBlocks: undefined,
|
|
960
|
+
hasToolUse: false,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
handleServerRequest(child, id, method, params) {
|
|
964
|
+
if (method === 'item/tool/call') {
|
|
965
|
+
this.handleDynamicToolRequest({ child, id, params });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const bodies = {
|
|
969
|
+
'item/tool/requestUserInput': { answers: {} },
|
|
970
|
+
'mcpServer/elicitation/request': { action: 'decline', content: null, _meta: null },
|
|
971
|
+
'item/commandExecution/requestApproval': { decision: 'decline' },
|
|
972
|
+
'item/fileChange/requestApproval': { decision: 'decline' },
|
|
973
|
+
'item/permissions/requestApproval': {
|
|
974
|
+
permissions: {},
|
|
975
|
+
scope: 'turn',
|
|
976
|
+
strictAutoReview: true,
|
|
977
|
+
},
|
|
978
|
+
applyPatchApproval: { decision: 'denied' },
|
|
979
|
+
execCommandApproval: { decision: 'denied' },
|
|
980
|
+
};
|
|
981
|
+
if (Object.hasOwn(bodies, method)) {
|
|
982
|
+
this.reply(child, { jsonrpc: '2.0', id, result: bodies[method] });
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
this.reply(child, {
|
|
986
|
+
jsonrpc: '2.0',
|
|
987
|
+
id,
|
|
988
|
+
error: { code: -32601, message: `Unsupported app-server request: ${method}` },
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
handleDynamicToolRequest(request, expectedTurn) {
|
|
993
|
+
const data = object(request.params);
|
|
994
|
+
const threadId = typeof data?.threadId === 'string' ? data.threadId : undefined;
|
|
995
|
+
const turn = threadId ? this.turns.get(threadId) : undefined;
|
|
996
|
+
if (!data || !turn || !turn.hostToolBridge || (expectedTurn && turn !== expectedTurn)) {
|
|
997
|
+
this.replyDisabledTool(request);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const turnId = this.requiredToolString(data, 'turnId');
|
|
1002
|
+
const callId = this.requiredToolString(data, 'callId');
|
|
1003
|
+
const tool = this.requiredToolString(data, 'tool');
|
|
1004
|
+
if (data.namespace !== null && typeof data.namespace !== 'string') {
|
|
1005
|
+
throw new Error('Codex app-server tool call namespace must be null or a string');
|
|
1006
|
+
}
|
|
1007
|
+
const input = object(data.arguments);
|
|
1008
|
+
if (!input) {
|
|
1009
|
+
throw new Error('Codex app-server tool call arguments must be an object');
|
|
1010
|
+
}
|
|
1011
|
+
if (!turn.hostToolBridge.tools.some((definition) => definition.name === tool)) {
|
|
1012
|
+
throw new Error(`Codex app-server tool call ${tool} was not advertised`);
|
|
1013
|
+
}
|
|
1014
|
+
if (!turn.turnId) {
|
|
1015
|
+
turn.queuedToolRequests.push(request);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (turnId !== turn.turnId) {
|
|
1019
|
+
this.failToolProtocol(request, turn, new Error('Codex app-server tool call did not match the active turn'));
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
this.dispatchDynamicToolCall(request, turn, callId, tool, data.namespace, input);
|
|
1023
|
+
}
|
|
1024
|
+
catch (error) {
|
|
1025
|
+
this.failToolProtocol(request, turn, this.toError(error));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
failToolProtocol(request, turn, error) {
|
|
1029
|
+
const failure = this.toError(error);
|
|
1030
|
+
this.replyToolError(request, failure.message);
|
|
1031
|
+
this.failTurn(turn.threadId, failure);
|
|
1032
|
+
if (this.child === request.child && !this.shutdownPromise) {
|
|
1033
|
+
void this.shutdown(failure);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
requiredToolString(data, field) {
|
|
1037
|
+
const value = data[field];
|
|
1038
|
+
if (typeof value !== 'string' || !value) {
|
|
1039
|
+
throw new Error(`Codex app-server tool call ${field} must be a string`);
|
|
1040
|
+
}
|
|
1041
|
+
return value;
|
|
1042
|
+
}
|
|
1043
|
+
dispatchDynamicToolCall(request, turn, callId, tool, namespace, input) {
|
|
1044
|
+
const identity = JSON.stringify(stableJson({
|
|
1045
|
+
threadId: turn.threadId,
|
|
1046
|
+
turnId: turn.turnId,
|
|
1047
|
+
tool,
|
|
1048
|
+
namespace,
|
|
1049
|
+
arguments: input,
|
|
1050
|
+
}));
|
|
1051
|
+
const existing = turn.toolCallResults.get(callId);
|
|
1052
|
+
if (existing && existing.identity !== identity) {
|
|
1053
|
+
throw new Error(`Codex app-server callId ${callId} had a conflicting request`);
|
|
1054
|
+
}
|
|
1055
|
+
let execution = existing?.execution;
|
|
1056
|
+
if (!execution) {
|
|
1057
|
+
const bridge = turn.hostToolBridge;
|
|
1058
|
+
if (!bridge) {
|
|
1059
|
+
this.replyDisabledTool(request);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
execution = turn.toolCallQueue
|
|
1063
|
+
.then(async () => {
|
|
1064
|
+
if (!this.isToolTurnActive(request, turn)) {
|
|
1065
|
+
return {
|
|
1066
|
+
result: this.toolResult(false, 'Codex app-server tool call is no longer active'),
|
|
1067
|
+
stop: false,
|
|
1068
|
+
abortError: undefined,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
let result;
|
|
1072
|
+
try {
|
|
1073
|
+
result = await bridge.execute({
|
|
1074
|
+
callId,
|
|
1075
|
+
name: tool,
|
|
1076
|
+
input,
|
|
1077
|
+
signal: turn.abortController.signal,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
result = { content: this.toError(error).message, isError: true };
|
|
1082
|
+
}
|
|
1083
|
+
const resultData = object(result);
|
|
1084
|
+
if (!resultData ||
|
|
1085
|
+
typeof resultData.content !== 'string' ||
|
|
1086
|
+
typeof resultData.isError !== 'boolean') {
|
|
1087
|
+
return {
|
|
1088
|
+
result: this.toolResult(false, 'Host tool returned a malformed result'),
|
|
1089
|
+
stop: false,
|
|
1090
|
+
abortError: undefined,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
const abortError = resultData.abort === true && resultData.isError
|
|
1094
|
+
? new Error(resultData.content)
|
|
1095
|
+
: undefined;
|
|
1096
|
+
return {
|
|
1097
|
+
result: this.toolResult(!resultData.isError, resultData.content),
|
|
1098
|
+
stop: resultData.stop === true && !resultData.isError,
|
|
1099
|
+
abortError,
|
|
1100
|
+
};
|
|
1101
|
+
})
|
|
1102
|
+
.catch((error) => ({
|
|
1103
|
+
result: this.toolResult(false, this.toError(error).message),
|
|
1104
|
+
stop: false,
|
|
1105
|
+
abortError: undefined,
|
|
1106
|
+
}));
|
|
1107
|
+
turn.toolCallResults.set(callId, { identity, execution });
|
|
1108
|
+
turn.toolCallQueue = execution.then(() => undefined, () => undefined);
|
|
1109
|
+
}
|
|
1110
|
+
void execution.then(({ result, stop, abortError }) => {
|
|
1111
|
+
if (!this.isToolTurnActive(request, turn)) {
|
|
1112
|
+
if (this.child === request.child) {
|
|
1113
|
+
this.replyToolError(request, 'Codex app-server tool call is no longer active');
|
|
1114
|
+
}
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
this.reply(request.child, { jsonrpc: '2.0', id: request.id, result });
|
|
1118
|
+
if (abortError && !turn.stoppingCallIds.has(callId)) {
|
|
1119
|
+
turn.stoppingCallIds.add(callId);
|
|
1120
|
+
turn.abortError = abortError;
|
|
1121
|
+
void this.request('turn/interrupt', { threadId: turn.threadId, turnId: turn.turnId }).catch((error) => this.failTurn(turn.threadId, this.toError(error)));
|
|
1122
|
+
}
|
|
1123
|
+
else if (stop && !turn.stoppingCallIds.has(callId)) {
|
|
1124
|
+
turn.stoppingCallIds.add(callId);
|
|
1125
|
+
turn.intentionalStop = true;
|
|
1126
|
+
void this.request('turn/interrupt', { threadId: turn.threadId, turnId: turn.turnId }).catch((error) => this.failTurn(turn.threadId, this.toError(error)));
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
isToolTurnActive(request, turn) {
|
|
1131
|
+
return (this.child === request.child &&
|
|
1132
|
+
this.turns.get(turn.threadId) === turn &&
|
|
1133
|
+
!turn.intentionalStop &&
|
|
1134
|
+
!turn.abortError &&
|
|
1135
|
+
!turn.abortController.signal.aborted);
|
|
1136
|
+
}
|
|
1137
|
+
toolResult(success, content) {
|
|
1138
|
+
return { success, contentItems: [{ type: 'inputText', text: content }] };
|
|
1139
|
+
}
|
|
1140
|
+
replyDisabledTool(request) {
|
|
1141
|
+
this.reply(request.child, {
|
|
1142
|
+
jsonrpc: '2.0',
|
|
1143
|
+
id: request.id,
|
|
1144
|
+
result: this.toolResult(false, 'Native app-server tools are disabled by MAMA'),
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
replyToolError(request, message) {
|
|
1148
|
+
this.reply(request.child, {
|
|
1149
|
+
jsonrpc: '2.0',
|
|
1150
|
+
id: request.id,
|
|
1151
|
+
error: { code: -32602, message },
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
reply(child, message) {
|
|
1155
|
+
if (child.stdin.writable) {
|
|
1156
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
handleStderr(line) {
|
|
1160
|
+
this.stderrTail = `${this.stderrTail}${this.redact(line)}\n`.slice(-STDERR_LIMIT);
|
|
1161
|
+
}
|
|
1162
|
+
redact(value) {
|
|
1163
|
+
let result = value;
|
|
1164
|
+
for (const secret of this.secrets) {
|
|
1165
|
+
result = result.split(secret).join('[REDACTED]');
|
|
1166
|
+
}
|
|
1167
|
+
return result;
|
|
1168
|
+
}
|
|
1169
|
+
toError(error) {
|
|
1170
|
+
return error instanceof Error
|
|
1171
|
+
? new Error(this.redact(error.message))
|
|
1172
|
+
: new Error(this.redact(String(error)));
|
|
1173
|
+
}
|
|
1174
|
+
failTurn(threadId, error) {
|
|
1175
|
+
const turn = this.turns.get(threadId);
|
|
1176
|
+
if (!turn) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
this.turns.delete(threadId);
|
|
1180
|
+
clearTimeout(turn.timer);
|
|
1181
|
+
turn.abortController.abort(error);
|
|
1182
|
+
turn.queuedNotifications.length = 0;
|
|
1183
|
+
turn.queuedToolRequests.length = 0;
|
|
1184
|
+
const safe = this.toError(error);
|
|
1185
|
+
// A gateway mutation that cannot be interrupted must settle before the
|
|
1186
|
+
// caller sees failure. This prevents retries from racing a late mutation.
|
|
1187
|
+
void turn.toolCallQueue.finally(() => {
|
|
1188
|
+
this.clearTurnCallbacks(turn);
|
|
1189
|
+
turn.reject(safe);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
timeoutTurn(threadId, error, requestTimeout) {
|
|
1193
|
+
const turn = this.turns.get(threadId);
|
|
1194
|
+
if (!turn) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const turnId = turn.turnId;
|
|
1198
|
+
if (!turnId) {
|
|
1199
|
+
this.getOrCreateTurnStartReconciliation(threadId);
|
|
1200
|
+
}
|
|
1201
|
+
this.failTurn(threadId, error);
|
|
1202
|
+
if (!turnId) {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
void this.request('turn/interrupt', { threadId, turnId }, requestTimeout).catch((interruptError) => {
|
|
1206
|
+
if (this.child && !this.shutdownPromise) {
|
|
1207
|
+
void this.shutdown(this.toError(interruptError));
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
getOrCreateTurnStartReconciliation(threadId) {
|
|
1212
|
+
const existing = this.turnStartReconciliations.get(threadId);
|
|
1213
|
+
if (existing) {
|
|
1214
|
+
return existing;
|
|
1215
|
+
}
|
|
1216
|
+
let resolveReconciliation;
|
|
1217
|
+
const promise = new Promise((resolvePromise) => {
|
|
1218
|
+
resolveReconciliation = resolvePromise;
|
|
1219
|
+
});
|
|
1220
|
+
const reconciliation = {
|
|
1221
|
+
promise,
|
|
1222
|
+
resolve: () => resolveReconciliation?.(),
|
|
1223
|
+
};
|
|
1224
|
+
this.turnStartReconciliations.set(threadId, reconciliation);
|
|
1225
|
+
return reconciliation;
|
|
1226
|
+
}
|
|
1227
|
+
completeTurnStartReconciliation(threadId, reconciliation) {
|
|
1228
|
+
if (this.turnStartReconciliations.get(threadId) !== reconciliation) {
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (reconciliation.recoveryTimer) {
|
|
1232
|
+
clearTimeout(reconciliation.recoveryTimer);
|
|
1233
|
+
reconciliation.recoveryTimer = undefined;
|
|
1234
|
+
}
|
|
1235
|
+
this.turnStartReconciliations.delete(threadId);
|
|
1236
|
+
reconciliation.resolve();
|
|
1237
|
+
}
|
|
1238
|
+
async reconcileAcknowledgedTurn(threadId, turnId, requestTimeout) {
|
|
1239
|
+
const reconciliation = this.turnStartReconciliations.get(threadId);
|
|
1240
|
+
if (!reconciliation) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
try {
|
|
1244
|
+
await this.request('turn/interrupt', { threadId, turnId }, requestTimeout);
|
|
1245
|
+
this.completeTurnStartReconciliation(threadId, reconciliation);
|
|
1246
|
+
}
|
|
1247
|
+
catch (error) {
|
|
1248
|
+
await this.shutdown(this.toError(error));
|
|
1249
|
+
this.completeTurnStartReconciliation(threadId, reconciliation);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
handleLateTurnStartResponse(id, late, message) {
|
|
1253
|
+
this.lateTurnStarts.delete(id);
|
|
1254
|
+
if (late.reconciliation.recoveryTimer) {
|
|
1255
|
+
clearTimeout(late.reconciliation.recoveryTimer);
|
|
1256
|
+
late.reconciliation.recoveryTimer = undefined;
|
|
1257
|
+
}
|
|
1258
|
+
if (message.error !== undefined) {
|
|
1259
|
+
this.completeTurnStartReconciliation(late.threadId, late.reconciliation);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
try {
|
|
1263
|
+
const turn = validateTurn(object(message.result)?.turn, 'late turn/start turn');
|
|
1264
|
+
if (typeof turn?.id !== 'string' || !turn.id) {
|
|
1265
|
+
throw new Error('Codex app-server late turn/start returned no turn id');
|
|
1266
|
+
}
|
|
1267
|
+
void this.reconcileAcknowledgedTurn(late.threadId, turn.id, late.requestTimeout);
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
void this.shutdown(this.toError(error)).finally(() => {
|
|
1271
|
+
this.completeTurnStartReconciliation(late.threadId, late.reconciliation);
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
clearTurnCallbacks(turn) {
|
|
1276
|
+
turn.queuedNotifications.length = 0;
|
|
1277
|
+
turn.queuedToolRequests.length = 0;
|
|
1278
|
+
turn.toolCallResults.clear();
|
|
1279
|
+
turn.stoppingCallIds.clear();
|
|
1280
|
+
turn.hostToolBridge = undefined;
|
|
1281
|
+
turn.onDelta = undefined;
|
|
1282
|
+
}
|
|
1283
|
+
failAll(error) {
|
|
1284
|
+
const safe = this.toError(error);
|
|
1285
|
+
for (const threadId of [...this.turns.keys()]) {
|
|
1286
|
+
this.failTurn(threadId, safe);
|
|
1287
|
+
}
|
|
1288
|
+
for (const [id, pending] of this.pending) {
|
|
1289
|
+
clearTimeout(pending.timer);
|
|
1290
|
+
this.pending.delete(id);
|
|
1291
|
+
pending.reject(safe);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
failProcess(child, error) {
|
|
1295
|
+
if (child !== this.child) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const suffix = this.stderrTail ? `: ${this.stderrTail}` : '';
|
|
1299
|
+
this.failAll(new Error(this.redact(`${error.message}${suffix}`)));
|
|
1300
|
+
this.detach(child);
|
|
1301
|
+
}
|
|
1302
|
+
async restart() {
|
|
1303
|
+
await this.shutdown(new Error('Codex app-server restarting after auth refresh'));
|
|
1304
|
+
this.sessions.clear();
|
|
1305
|
+
}
|
|
1306
|
+
async shutdown(reason) {
|
|
1307
|
+
if (this.shutdownPromise) {
|
|
1308
|
+
return this.shutdownPromise;
|
|
1309
|
+
}
|
|
1310
|
+
const operation = this.performShutdown(reason);
|
|
1311
|
+
this.shutdownPromise = operation;
|
|
1312
|
+
try {
|
|
1313
|
+
await operation;
|
|
1314
|
+
}
|
|
1315
|
+
finally {
|
|
1316
|
+
if (this.shutdownPromise === operation) {
|
|
1317
|
+
this.shutdownPromise = undefined;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
async performShutdown(reason) {
|
|
1322
|
+
const child = this.child;
|
|
1323
|
+
if (!child) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
this.failAll(reason);
|
|
1327
|
+
await new Promise((resolveStop) => {
|
|
1328
|
+
let settled = false;
|
|
1329
|
+
const finish = () => {
|
|
1330
|
+
if (settled) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
settled = true;
|
|
1334
|
+
if (this.killTimer) {
|
|
1335
|
+
clearTimeout(this.killTimer);
|
|
1336
|
+
this.killTimer = undefined;
|
|
1337
|
+
}
|
|
1338
|
+
if (this.finalKillTimer) {
|
|
1339
|
+
clearTimeout(this.finalKillTimer);
|
|
1340
|
+
this.finalKillTimer = undefined;
|
|
1341
|
+
}
|
|
1342
|
+
this.detach(child);
|
|
1343
|
+
resolveStop();
|
|
1344
|
+
};
|
|
1345
|
+
child.once('exit', finish);
|
|
1346
|
+
child.kill('SIGTERM');
|
|
1347
|
+
this.killTimer = setTimeout(() => {
|
|
1348
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
1349
|
+
child.kill('SIGKILL');
|
|
1350
|
+
}
|
|
1351
|
+
this.killTimer = undefined;
|
|
1352
|
+
this.finalKillTimer = setTimeout(finish, STOP_GRACE_MS);
|
|
1353
|
+
this.finalKillTimer.unref();
|
|
1354
|
+
}, STOP_GRACE_MS);
|
|
1355
|
+
this.killTimer.unref();
|
|
1356
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1357
|
+
finish();
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
detach(child) {
|
|
1362
|
+
if (this.child !== child) {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
this.stdout?.close();
|
|
1366
|
+
this.stderr?.close();
|
|
1367
|
+
child.removeAllListeners();
|
|
1368
|
+
child.stdin.removeAllListeners();
|
|
1369
|
+
child.stdout.removeAllListeners();
|
|
1370
|
+
child.stderr.removeAllListeners();
|
|
1371
|
+
this.stdout = undefined;
|
|
1372
|
+
this.stderr = undefined;
|
|
1373
|
+
this.child = undefined;
|
|
1374
|
+
this.sessions.clear();
|
|
1375
|
+
this.lateTurnStarts.clear();
|
|
1376
|
+
for (const [threadId, reconciliation] of this.turnStartReconciliations) {
|
|
1377
|
+
this.completeTurnStartReconciliation(threadId, reconciliation);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
exports.CodexAppServerProcess = CodexAppServerProcess;
|
|
1382
|
+
//# sourceMappingURL=codex-app-server-process.js.map
|