@ckanner/flywheel 0.32.1 → 0.33.1
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 +20 -1
- package/README.md +7 -0
- package/README.zh-CN.md +5 -0
- package/THIRD_PARTY_NOTICES.md +42 -0
- package/bin/runtime.js +25 -31
- package/cordis.patch.yml +5 -0
- package/dist/bridge.mjs +421 -0
- package/dist/claude-code-pro-bridge.mjs +82 -0
- package/dist/claude-code-pro-worker.mjs +5 -0
- package/dist/client.js +4140 -0
- package/dist/codex-pro-bridge.mjs +75 -0
- package/dist/codex-pro-worker.mjs +5 -0
- package/dist/console-read.mjs +51 -0
- package/dist/control.mjs +218 -0
- package/dist/index.mjs +185 -0
- package/dist/installation.mjs +56 -0
- package/dist/machine-worker.mjs +241 -0
- package/dist/policy.mjs +91 -0
- package/dist/prewarm.mjs +136 -0
- package/dist/protocol.mjs +260 -0
- package/dist/task-association.mjs +127 -0
- package/dist/worker.mjs +52 -0
- package/dist/workspaces.mjs +42 -0
- package/docs/README.md +1 -1
- package/docs/README.zh-CN.md +1 -1
- package/docs/design.md +8 -5
- package/docs/en/configuration.md +1 -1
- package/docs/en/contributing.md +5 -6
- package/docs/en/flow-console.md +2 -2
- package/docs/en/getting-started.md +9 -2
- package/docs/en/releasing.md +132 -174
- package/docs/validation/2026-11-13-release-0.32.1.md +83 -0
- package/docs/validation/2026-11-14-release-0.33.0.md +49 -0
- package/docs/validation/2026-11-14-single-package.md +60 -0
- package/docs/validation/README.md +3 -0
- package/docs/zh-CN/configuration.md +1 -1
- package/docs/zh-CN/contributing.md +4 -5
- package/docs/zh-CN/flow-console.md +2 -2
- package/docs/zh-CN/getting-started.md +7 -3
- package/docs/zh-CN/releasing.md +97 -81
- package/native/darwin-amd64/flywheel +0 -0
- package/native/darwin-arm64/flywheel +0 -0
- package/native/linux-amd64/flywheel +0 -0
- package/native/linux-arm64/flywheel +0 -0
- package/native/manifest.json +22 -0
- package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +403 -0
- package/node_modules/dsh-subagent-codex-pro/LICENSE +21 -0
- package/node_modules/dsh-subagent-codex-pro/README.md +557 -0
- package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +30 -0
- package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +355 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +65 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +144 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +98 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine.md +194 -0
- package/node_modules/dsh-subagent-codex-pro/docs/orchestration.zh.md +86 -0
- package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js +68 -0
- package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +15 -0
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/index.js +830 -0
- package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +35 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js +229 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +101 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +122 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js +167 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +141 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +128 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js +104 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +156 -0
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/run.js +842 -0
- package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/structured.js +329 -0
- package/node_modules/dsh-subagent-codex-pro/lib/structured.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js +295 -0
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts +88 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +179 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +266 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +34 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts +113 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts +3 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts +4 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts +53 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts +8 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts +2 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +70 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts +22 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +386 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts +35 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +107 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts +131 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +321 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/usage.js +356 -0
- package/node_modules/dsh-subagent-codex-pro/lib/usage.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js +986 -0
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/package.json +76 -0
- package/npm/install.js +59 -6
- package/package.json +74 -12
- package/plugins/dsh/LICENSE +21 -0
- package/plugins/dsh/PROTOCOL.md +609 -0
- package/plugins/dsh/README.en.md +4 -0
- package/plugins/dsh/README.md +342 -0
- package/plugins/dsh/README.zh-CN.md +279 -0
- package/plugins/dsh/THIRD_PARTY_NOTICES.md +42 -0
- package/plugins/dsh/vendor/README.md +17 -0
- package/plugins/dsh/vendor/codex-pro.json +16 -0
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex app-server protocol adapter.
|
|
3
|
+
*
|
|
4
|
+
* A faithful port of `@deepseek-ai/dsh-subagent-codex`'s wire module, extended
|
|
5
|
+
* with the per-call choices this package exists for: `model` and the unattended
|
|
6
|
+
* permission policy on `thread/start`, per-turn `effort` on `turn/start`, and
|
|
7
|
+
* explicit model/cwd/permission overrides plus effective-configuration checks
|
|
8
|
+
* on `thread/resume` (0.165+). A thinking level is **not** a `thread/start`
|
|
9
|
+
* field in 0.165/0.166. The module also accounts for token usage and account
|
|
10
|
+
* rate limits, keeping them separate.
|
|
11
|
+
*
|
|
12
|
+
* Everything else is carried over as-is, because the surrounding behaviour is
|
|
13
|
+
* load-bearing and was tuned against a real app-server: the initialize
|
|
14
|
+
* handshake, thread/turn association (including notifications that arrive
|
|
15
|
+
* before `turn/start` returns), unattended approval decisions, terminal-answer
|
|
16
|
+
* selection by phase, and the failure taxonomy.
|
|
17
|
+
*
|
|
18
|
+
* Differences from the source, all mechanical:
|
|
19
|
+
* - `JsonRpcLineTransport` is this package's own `LineTransport`, so the
|
|
20
|
+
* transport method shape is `request(method, params, { signal })` rather than
|
|
21
|
+
* a positional signal.
|
|
22
|
+
* - Error text uses this package's prefix.
|
|
23
|
+
* - Model and permission choices are constructor state; `effort` travels through
|
|
24
|
+
* `runTurn`; resume overrides are per call.
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-subagent-codex-pro/wire
|
|
27
|
+
*/
|
|
28
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
29
|
+
import { realpathSync } from 'node:fs';
|
|
30
|
+
import { TokenUsageTracker } from "./usage.js";
|
|
31
|
+
/** Prefix on every error this module raises. */
|
|
32
|
+
const P = 'codex-pro';
|
|
33
|
+
/** Explicit thread permissions; approval policy never implies a sandbox. */
|
|
34
|
+
const THREAD_PERMISSION_PARAMS = {
|
|
35
|
+
'read-only': {
|
|
36
|
+
approvalPolicy: 'never',
|
|
37
|
+
sandbox: 'read-only',
|
|
38
|
+
},
|
|
39
|
+
'workspace-write': {
|
|
40
|
+
approvalPolicy: 'never',
|
|
41
|
+
sandbox: 'workspace-write',
|
|
42
|
+
},
|
|
43
|
+
never: { approvalPolicy: 'never' },
|
|
44
|
+
'approve-for-me': {
|
|
45
|
+
approvalPolicy: 'on-request',
|
|
46
|
+
approvalsReviewer: 'auto_review',
|
|
47
|
+
sandbox: 'workspace-write',
|
|
48
|
+
},
|
|
49
|
+
'dangerously-bypass-approvals-and-sandbox': {
|
|
50
|
+
approvalPolicy: 'never',
|
|
51
|
+
sandbox: 'danger-full-access',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
/** Assert a protocol value is a non-array object. */
|
|
55
|
+
function object(value, label) {
|
|
56
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
57
|
+
throw new Error(`${P}: app-server returned invalid ${label}`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
/** Assert a protocol value is a non-empty string. */
|
|
62
|
+
function string(value, label) {
|
|
63
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
64
|
+
throw new Error(`${P}: app-server returned invalid ${label}`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The only decision an unattended provider can make from what the server offers.
|
|
70
|
+
*
|
|
71
|
+
* @param params - the approval request's params.
|
|
72
|
+
* @returns the decision to send back.
|
|
73
|
+
*/
|
|
74
|
+
function unattendedDecision(params) {
|
|
75
|
+
const available = params.availableDecisions;
|
|
76
|
+
if (available === undefined || available === null)
|
|
77
|
+
return 'decline';
|
|
78
|
+
if (Array.isArray(available)) {
|
|
79
|
+
if (available.includes('cancel'))
|
|
80
|
+
return 'cancel';
|
|
81
|
+
if (available.includes('decline'))
|
|
82
|
+
return 'decline';
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`${P}: app-server offered no unattended approval decision`);
|
|
85
|
+
}
|
|
86
|
+
/** A plausible HTTP status, or undefined. */
|
|
87
|
+
function numericHttpStatus(value) {
|
|
88
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 65_535 ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
/** Classify a structured `codexErrorInfo` object. */
|
|
91
|
+
function objectFailureInfo(value) {
|
|
92
|
+
const keys = Object.keys(value);
|
|
93
|
+
const category = keys[0];
|
|
94
|
+
if (keys.length !== 1 || category === undefined)
|
|
95
|
+
return { category: 'unknown' };
|
|
96
|
+
const detail = value[category];
|
|
97
|
+
if (detail === null || typeof detail !== 'object' || Array.isArray(detail))
|
|
98
|
+
return { category: 'unknown' };
|
|
99
|
+
const fields = detail;
|
|
100
|
+
switch (category) {
|
|
101
|
+
case 'httpConnectionFailed':
|
|
102
|
+
case 'responseStreamConnectionFailed':
|
|
103
|
+
case 'responseStreamDisconnected':
|
|
104
|
+
case 'responseTooManyFailedAttempts': {
|
|
105
|
+
const httpStatus = numericHttpStatus(fields.httpStatusCode);
|
|
106
|
+
return httpStatus === undefined ? { category: 'transport' } : { category: 'transport', httpStatus };
|
|
107
|
+
}
|
|
108
|
+
case 'activeTurnNotSteerable':
|
|
109
|
+
return { category: 'product-error' };
|
|
110
|
+
default:
|
|
111
|
+
return { category: 'unknown' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Read the failure taxonomy out of a terminal turn. */
|
|
115
|
+
function failureInfo(turn) {
|
|
116
|
+
if (turn.status !== 'failed')
|
|
117
|
+
return { category: 'unknown' };
|
|
118
|
+
const error = turn.error;
|
|
119
|
+
if (error === null || typeof error !== 'object' || Array.isArray(error))
|
|
120
|
+
return { category: 'unknown' };
|
|
121
|
+
const info = error.codexErrorInfo;
|
|
122
|
+
if (typeof info === 'string') {
|
|
123
|
+
switch (info) {
|
|
124
|
+
case 'contextWindowExceeded':
|
|
125
|
+
return { category: 'limit', maxTokens: true };
|
|
126
|
+
case 'sessionBudgetExceeded':
|
|
127
|
+
case 'usageLimitExceeded':
|
|
128
|
+
return { category: 'limit' };
|
|
129
|
+
case 'serverOverloaded':
|
|
130
|
+
case 'internalServerError':
|
|
131
|
+
return { category: 'service' };
|
|
132
|
+
case 'cyberPolicy':
|
|
133
|
+
case 'misalignmentPolicyViolation':
|
|
134
|
+
case 'unauthorized':
|
|
135
|
+
return { category: 'access-policy' };
|
|
136
|
+
case 'badRequest':
|
|
137
|
+
case 'threadRollbackFailed':
|
|
138
|
+
case 'other':
|
|
139
|
+
return { category: 'product-error' };
|
|
140
|
+
case 'sandboxError':
|
|
141
|
+
return { category: 'access-policy', sandboxFailure: true };
|
|
142
|
+
default:
|
|
143
|
+
return { category: 'unknown' };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return info !== null && typeof info === 'object' && !Array.isArray(info)
|
|
147
|
+
? objectFailureInfo(info)
|
|
148
|
+
: { category: 'unknown' };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* One progress line for a completed non-message item.
|
|
152
|
+
*
|
|
153
|
+
* @param item - the completed item.
|
|
154
|
+
* @returns a short human-readable line.
|
|
155
|
+
*/
|
|
156
|
+
function describeItem(item) {
|
|
157
|
+
const type = typeof item.type === 'string' ? item.type : 'item';
|
|
158
|
+
const status = typeof item.status === 'string' ? ` ${item.status}` : '';
|
|
159
|
+
const command = typeof item.command === 'string' ? `: ${item.command.slice(0, 120)}` : '';
|
|
160
|
+
const path = typeof item.path === 'string' ? `: ${item.path.slice(0, 120)}` : '';
|
|
161
|
+
return `${type}${status}${command}${path}`;
|
|
162
|
+
}
|
|
163
|
+
/** Normalise an unknown throwable. */
|
|
164
|
+
function thrown(value) {
|
|
165
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
166
|
+
}
|
|
167
|
+
/** The failure produced when a signal aborts a request. */
|
|
168
|
+
function abortError(signal) {
|
|
169
|
+
return signal.reason instanceof Error
|
|
170
|
+
? signal.reason
|
|
171
|
+
: new Error(`${P}: app-server request aborted: ${String(signal.reason)}`);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Race one pending operation against a cancellation signal.
|
|
175
|
+
*
|
|
176
|
+
* @param pending - the operation.
|
|
177
|
+
* @param signal - cancellation.
|
|
178
|
+
* @returns the operation's value.
|
|
179
|
+
*/
|
|
180
|
+
export async function raceAbort(pending, signal) {
|
|
181
|
+
if (signal === undefined)
|
|
182
|
+
return pending;
|
|
183
|
+
if (signal.aborted) {
|
|
184
|
+
pending.catch(() => { });
|
|
185
|
+
throw abortError(signal);
|
|
186
|
+
}
|
|
187
|
+
let rejectAbort = () => { };
|
|
188
|
+
const aborted = new Promise((_resolve, reject) => { rejectAbort = reject; });
|
|
189
|
+
const onAbort = () => { rejectAbort(abortError(signal)); };
|
|
190
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
191
|
+
try {
|
|
192
|
+
return await Promise.race([pending, aborted]);
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
signal.removeEventListener('abort', onAbort);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** The originator the Codex desktop client lists. */
|
|
199
|
+
export const DEFAULT_ORIGINATOR = 'Codex Desktop';
|
|
200
|
+
/** The subset of a permission mode that thread requests actually send. */
|
|
201
|
+
function requestedPermission(permission) {
|
|
202
|
+
return {
|
|
203
|
+
approvalPolicy: permission.approvalPolicy,
|
|
204
|
+
...'approvalsReviewer' in permission ? { approvalsReviewer: permission.approvalsReviewer } : {},
|
|
205
|
+
...'sandbox' in permission ? { sandbox: permission.sandbox } : {},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Map the app-server's sandbox policy object back to the request's mode name. */
|
|
209
|
+
function sandboxModeOf(value) {
|
|
210
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
211
|
+
return undefined;
|
|
212
|
+
const type = value.type;
|
|
213
|
+
switch (type) {
|
|
214
|
+
case 'readOnly': return 'read-only';
|
|
215
|
+
case 'workspaceWrite': return 'workspace-write';
|
|
216
|
+
case 'dangerFullAccess': return 'danger-full-access';
|
|
217
|
+
case 'externalSandbox': return 'external-sandbox';
|
|
218
|
+
default: return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Read the effective configuration fields a start/resume response carries. */
|
|
222
|
+
function effectiveConfiguration(response) {
|
|
223
|
+
const text = (field) => typeof response[field] === 'string' && response[field] !== '' ? response[field] : undefined;
|
|
224
|
+
const model = text('model');
|
|
225
|
+
const reportedCwd = text('cwd');
|
|
226
|
+
const cwd = reportedCwd !== undefined && isAbsolute(reportedCwd) ? reportedCwd : undefined;
|
|
227
|
+
const approvalPolicy = text('approvalPolicy');
|
|
228
|
+
const approvalsReviewer = text('approvalsReviewer');
|
|
229
|
+
const sandbox = sandboxModeOf(response.sandbox);
|
|
230
|
+
const reasoningEffort = response.reasoningEffort;
|
|
231
|
+
return {
|
|
232
|
+
...model === undefined ? {} : { model },
|
|
233
|
+
...cwd === undefined ? {} : { cwd },
|
|
234
|
+
...approvalPolicy === undefined ? {} : { approvalPolicy },
|
|
235
|
+
...approvalsReviewer === undefined ? {} : { approvalsReviewer },
|
|
236
|
+
...sandbox === undefined ? {} : { sandbox },
|
|
237
|
+
...sandbox === undefined ? {} : { sandboxPolicy: structuredClone(response.sandbox) },
|
|
238
|
+
...reasoningEffort === null || typeof reasoningEffort === 'string'
|
|
239
|
+
? { reasoningEffort: reasoningEffort }
|
|
240
|
+
: {},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** Codex may return a physical macOS path such as /private/tmp for /tmp. */
|
|
244
|
+
function sameDirectory(actual, requested) {
|
|
245
|
+
if (resolve(actual) === resolve(requested))
|
|
246
|
+
return true;
|
|
247
|
+
try {
|
|
248
|
+
return realpathSync(actual) === realpathSync(requested);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Compare requested facts with the effective ones and retain both.
|
|
256
|
+
*
|
|
257
|
+
* The generated 0.165/0.166 start/resume responses carry effective model, cwd,
|
|
258
|
+
* approval policy, reviewer and sandbox fields. A missing or malformed requested
|
|
259
|
+
* field is a failed capability check, not permission to proceed. Retain the
|
|
260
|
+
* facts before throwing, without echoing arbitrary protocol values in errors.
|
|
261
|
+
*/
|
|
262
|
+
function appliedConfiguration(source, requested, response) {
|
|
263
|
+
const effective = effectiveConfiguration(response);
|
|
264
|
+
const missing = [];
|
|
265
|
+
const mismatches = [];
|
|
266
|
+
const equals = (field, actual, requestedValue) => field === 'cwd' ? sameDirectory(actual, requestedValue) : actual === requestedValue;
|
|
267
|
+
const compare = (field, requestedValue) => {
|
|
268
|
+
if (requestedValue === undefined)
|
|
269
|
+
return true;
|
|
270
|
+
const actual = effective[field];
|
|
271
|
+
if (actual === undefined) {
|
|
272
|
+
missing.push(field);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (!equals(field, actual, requestedValue)) {
|
|
276
|
+
mismatches.push(field);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
};
|
|
281
|
+
const verified = [
|
|
282
|
+
compare('model', requested.model),
|
|
283
|
+
compare('cwd', requested.cwd),
|
|
284
|
+
compare('approvalPolicy', requested.approvalPolicy),
|
|
285
|
+
compare('approvalsReviewer', requested.approvalsReviewer),
|
|
286
|
+
compare('sandbox', requested.sandbox),
|
|
287
|
+
];
|
|
288
|
+
return {
|
|
289
|
+
source, requested, effective, matched: verified.every(Boolean), missing,
|
|
290
|
+
mismatches,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/** Refuse unproven configuration before any user task is sent. */
|
|
294
|
+
function requireAppliedConfiguration(configuration) {
|
|
295
|
+
if (configuration.matched)
|
|
296
|
+
return;
|
|
297
|
+
const detail = [
|
|
298
|
+
...(configuration.missing.length === 0 ? [] : [`missing or invalid ${configuration.missing.join(', ')}`]),
|
|
299
|
+
...(configuration.mismatches.length === 0 ? [] : [`mismatched ${configuration.mismatches.join(', ')}`]),
|
|
300
|
+
].join('; ');
|
|
301
|
+
throw new Error(`${P}: ${configuration.source} did not apply the requested configuration: ${detail}`);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* One app-server connection and its single ephemeral thread/turn.
|
|
305
|
+
*
|
|
306
|
+
* Deliberately exposes no generic request surface: supporting another product
|
|
307
|
+
* method must first become part of the provider contract.
|
|
308
|
+
*/
|
|
309
|
+
export class CodexAppServerWire {
|
|
310
|
+
#permissionMode;
|
|
311
|
+
#threadMode;
|
|
312
|
+
#originator;
|
|
313
|
+
#model;
|
|
314
|
+
#transport;
|
|
315
|
+
#fatal = Promise.withResolvers();
|
|
316
|
+
#threadId;
|
|
317
|
+
#turnId;
|
|
318
|
+
#dispatch = 'not-dispatched';
|
|
319
|
+
#pendingTurnId;
|
|
320
|
+
#turnCompleted;
|
|
321
|
+
#earlyTurnNotifications = [];
|
|
322
|
+
/** Usage notifications that arrived before thread/start or thread/resume returned. */
|
|
323
|
+
#earlyUsageNotifications = [];
|
|
324
|
+
#earlyUsageOverflow = false;
|
|
325
|
+
#lastFinalAnswer;
|
|
326
|
+
/** Stable partial answer text that later assistant output or tool calls may follow. */
|
|
327
|
+
#lastPartialAnswer;
|
|
328
|
+
#lastUnphasedAnswer;
|
|
329
|
+
#diagnostic;
|
|
330
|
+
/** Images the run produced, in the order the app-server reported them. */
|
|
331
|
+
#images = [];
|
|
332
|
+
#failure;
|
|
333
|
+
#diagnosticOrder = 0;
|
|
334
|
+
#observationOrder = 0;
|
|
335
|
+
#pendingDiagnostic;
|
|
336
|
+
#inputEnded = false;
|
|
337
|
+
#terminalObserved = false;
|
|
338
|
+
#closed = false;
|
|
339
|
+
/** Token usage folds cumulative snapshots and replays into one run report. */
|
|
340
|
+
#usage = new TokenUsageTracker();
|
|
341
|
+
/** The last rate-limit snapshot, kept separately from token usage. */
|
|
342
|
+
#rateLimits;
|
|
343
|
+
#rateLimitObservations = 0;
|
|
344
|
+
/** Requested/effective facts of the one start or resume this wire performed. */
|
|
345
|
+
#configuration;
|
|
346
|
+
#turnConfiguration;
|
|
347
|
+
/** Narrates notable events, for a caller that reports progress. */
|
|
348
|
+
#onProgress;
|
|
349
|
+
constructor(input, output, transport, options) {
|
|
350
|
+
this.#permissionMode = options.permissionMode;
|
|
351
|
+
this.#model = options.model;
|
|
352
|
+
this.#threadMode = options.threadMode ?? 'ephemeral';
|
|
353
|
+
this.#originator = options.originator ?? DEFAULT_ORIGINATOR;
|
|
354
|
+
this.#transport = transport;
|
|
355
|
+
this.#fatal.promise.catch(() => { });
|
|
356
|
+
this.#transport.setHooks({
|
|
357
|
+
onRequest: (method, params) => this.#handleServerRequest(method, params),
|
|
358
|
+
onNotification: (method, params) => {
|
|
359
|
+
try {
|
|
360
|
+
this.#handleNotification(method, params);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
this.#fail(thrown(error));
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
input.on('error', this.#onInputError);
|
|
368
|
+
input.on('end', this.#onInputEnd);
|
|
369
|
+
output.on('error', this.#onOutputError);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Install a progress narrator.
|
|
373
|
+
*
|
|
374
|
+
* A delegation can run for minutes, so a caller that reports progress needs
|
|
375
|
+
* the child's intermediate activity rather than only its final answer. Never
|
|
376
|
+
* lets a reporter's failure reach the protocol loop.
|
|
377
|
+
*
|
|
378
|
+
* @param onProgress - called with one line per notable event.
|
|
379
|
+
*/
|
|
380
|
+
setProgressReporter(onProgress) {
|
|
381
|
+
this.#onProgress = onProgress;
|
|
382
|
+
}
|
|
383
|
+
/** Report one progress line, swallowing a reporter's own failure. */
|
|
384
|
+
#progress(text) {
|
|
385
|
+
if (this.#onProgress === undefined)
|
|
386
|
+
return;
|
|
387
|
+
try {
|
|
388
|
+
this.#onProgress(text);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// A progress reporter must never break the run it observes.
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** Whether protocol output ended before a terminal turn notification. */
|
|
395
|
+
get endedBeforeTerminal() {
|
|
396
|
+
return this.#inputEnded && !this.#terminalObserved;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Perform the required initialize/initialized handshake.
|
|
400
|
+
*
|
|
401
|
+
* @param signal - cancellation for an unpublished start.
|
|
402
|
+
*/
|
|
403
|
+
async initialize(signal) {
|
|
404
|
+
object(await this.#guarded(this.#transport.request('initialize', {
|
|
405
|
+
clientInfo: { name: this.#originator, title: 'DeepSeek Harness', version: '0.0.1' },
|
|
406
|
+
capabilities: { experimentalApi: false, requestAttestation: false },
|
|
407
|
+
}, { signal }), signal), 'initialize response');
|
|
408
|
+
this.#transport.notify('initialized');
|
|
409
|
+
await this.#guarded(this.#transport.flush(), signal);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Create the run's thread and retain its identity.
|
|
413
|
+
*
|
|
414
|
+
* `ephemeral` follows this wire's thread mode. The app-server reports the
|
|
415
|
+
* decision back, and the check below is what proves it honoured the request:
|
|
416
|
+
* a `persistent` thread that came back ephemeral would be a silent downgrade,
|
|
417
|
+
* and the caller is about to promise the user a thread they can resume.
|
|
418
|
+
*
|
|
419
|
+
* @param cwd - the parent session's workspace.
|
|
420
|
+
* @param signal - cancellation for an unpublished start.
|
|
421
|
+
*/
|
|
422
|
+
async startThread(cwd, signal) {
|
|
423
|
+
const permission = THREAD_PERMISSION_PARAMS[this.#permissionMode];
|
|
424
|
+
const requested = {
|
|
425
|
+
cwd,
|
|
426
|
+
...(this.#model === undefined ? {} : { model: this.#model }),
|
|
427
|
+
...requestedPermission(permission),
|
|
428
|
+
};
|
|
429
|
+
const response = object(await this.#guarded(this.#transport.request('thread/start', {
|
|
430
|
+
cwd,
|
|
431
|
+
ephemeral: this.#threadMode === 'ephemeral',
|
|
432
|
+
...this.#model === undefined ? {} : { model: this.#model },
|
|
433
|
+
...permission,
|
|
434
|
+
}, { signal }), signal), 'thread/start response');
|
|
435
|
+
const thread = object(response.thread, 'thread/start thread');
|
|
436
|
+
const id = string(thread.id, 'thread/start thread id');
|
|
437
|
+
if (typeof thread.ephemeral !== 'boolean')
|
|
438
|
+
throw new Error(`${P}: app-server omitted explicit thread persistence evidence`);
|
|
439
|
+
const ephemeral = thread.ephemeral === true;
|
|
440
|
+
if (ephemeral !== (this.#threadMode === 'ephemeral')) {
|
|
441
|
+
throw new Error(`${P}: app-server created a ${ephemeral ? 'ephemeral' : 'persistent'} thread `
|
|
442
|
+
+ `where ${this.#threadMode} was asked for`);
|
|
443
|
+
}
|
|
444
|
+
this.#threadId = id;
|
|
445
|
+
this.#usage.resetForNewThread(id);
|
|
446
|
+
this.#flushEarlyUsage();
|
|
447
|
+
this.#configuration = appliedConfiguration('thread/start', requested, response);
|
|
448
|
+
this.#progress(`thread ${id} (${this.#threadMode}) configuration matched=${this.#configuration.matched}`);
|
|
449
|
+
requireAppliedConfiguration(this.#configuration);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Continue an existing thread instead of creating one.
|
|
453
|
+
*
|
|
454
|
+
* Codex 0.165+ accepts configuration overrides on `thread/resume`. Parameters
|
|
455
|
+
* that are present replace the thread's historical values; absent parameters
|
|
456
|
+
* leave history alone. The response is compared with the requested facts so a
|
|
457
|
+
* caller never mistakes a requested model, cwd, or permission policy for an
|
|
458
|
+
* effective one.
|
|
459
|
+
*
|
|
460
|
+
* The app-server refuses a thread with no rollout yet, so a thread becomes
|
|
461
|
+
* resumable only after it has carried one turn.
|
|
462
|
+
*
|
|
463
|
+
* @param threadId - the thread to continue, as a previous run reported it.
|
|
464
|
+
* @param overrides - explicit model/cwd/permission facts to apply.
|
|
465
|
+
* @param signal - cancellation for an unpublished resume.
|
|
466
|
+
*/
|
|
467
|
+
async resumeThread(threadId, overrides = {}, signal) {
|
|
468
|
+
const permission = overrides.permission === undefined
|
|
469
|
+
? undefined
|
|
470
|
+
: THREAD_PERMISSION_PARAMS[overrides.permission];
|
|
471
|
+
const requested = {
|
|
472
|
+
...(overrides.cwd === undefined ? {} : { cwd: overrides.cwd }),
|
|
473
|
+
...(overrides.model === undefined ? {} : { model: overrides.model }),
|
|
474
|
+
...(permission === undefined ? {} : requestedPermission(permission)),
|
|
475
|
+
};
|
|
476
|
+
const response = object(await this.#guarded(this.#transport.request('thread/resume', {
|
|
477
|
+
threadId,
|
|
478
|
+
...(overrides.cwd === undefined ? {} : { cwd: overrides.cwd }),
|
|
479
|
+
...(overrides.model === undefined ? {} : { model: overrides.model }),
|
|
480
|
+
...(permission ?? {}),
|
|
481
|
+
}, { signal }), signal), 'thread/resume response');
|
|
482
|
+
// The reply carries the thread, but a thread already known by id is what the
|
|
483
|
+
// caller asked for; the response only has to prove the resume was accepted.
|
|
484
|
+
const resumed = response.thread;
|
|
485
|
+
if (resumed !== undefined) {
|
|
486
|
+
const id = string(object(resumed, 'thread/resume thread').id, 'thread/resume thread id');
|
|
487
|
+
if (id !== threadId)
|
|
488
|
+
throw new Error(`${P}: thread/resume answered for ${id}, not ${threadId}`);
|
|
489
|
+
}
|
|
490
|
+
this.#threadId = threadId;
|
|
491
|
+
this.#usage.resetForResume(threadId);
|
|
492
|
+
this.#flushEarlyUsage();
|
|
493
|
+
this.#configuration = appliedConfiguration('thread/resume', requested, response);
|
|
494
|
+
this.#progress(`thread ${threadId} (resumed) configuration matched=${this.#configuration.matched}`);
|
|
495
|
+
requireAppliedConfiguration(this.#configuration);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Give the thread a name.
|
|
499
|
+
*
|
|
500
|
+
* The name is what a person sees in the Codex application's thread list. It
|
|
501
|
+
* matters here because a thread this provider creates has to report itself as
|
|
502
|
+
* `Codex Desktop` to be listed at all, so the name is the only place left to
|
|
503
|
+
* say that the work came from DSH and what it was. Best effort: a thread that
|
|
504
|
+
* ran is worth more than a naming failure, so the caller is told rather than
|
|
505
|
+
* thrown at.
|
|
506
|
+
*
|
|
507
|
+
* @param name - the name to record.
|
|
508
|
+
* @param signal - cancellation for an unpublished request.
|
|
509
|
+
* @returns whether the app-server accepted it.
|
|
510
|
+
*/
|
|
511
|
+
async setThreadName(name, signal) {
|
|
512
|
+
if (this.#threadId === undefined)
|
|
513
|
+
return false;
|
|
514
|
+
try {
|
|
515
|
+
await this.#guarded(this.#transport.request('thread/name/set', { threadId: this.#threadId, name }, { signal }), signal);
|
|
516
|
+
this.#progress(`thread ${this.#threadId} named ${JSON.stringify(name)}`);
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
// Naming is best effort, but a deadline/caller abort is not: swallowing
|
|
521
|
+
// it would let a timed-out startup publish a run after its budget.
|
|
522
|
+
if (signal?.aborted === true)
|
|
523
|
+
throw error;
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/** The images this run produced, in arrival order. */
|
|
528
|
+
get images() {
|
|
529
|
+
return this.#images;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Retain the images one completed item carries.
|
|
533
|
+
*
|
|
534
|
+
* `imageView` reports a path rather than bytes: the app-server expects the host
|
|
535
|
+
* that asked for the work to read the file itself, which is exactly what the
|
|
536
|
+
* caller can do. An MCP tool result is the other shape — a computer-use
|
|
537
|
+
* screenshot arrives as an image content block inside it.
|
|
538
|
+
*
|
|
539
|
+
* @param item - one completed item.
|
|
540
|
+
*/
|
|
541
|
+
#recordImages(item) {
|
|
542
|
+
if (item.type === 'imageView' && typeof item.path === 'string' && item.path !== '') {
|
|
543
|
+
this.#images.push({ path: item.path });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (item.type !== 'mcpToolCall')
|
|
547
|
+
return;
|
|
548
|
+
const result = item.result;
|
|
549
|
+
if (typeof result !== 'object' || result === null)
|
|
550
|
+
return;
|
|
551
|
+
const content = result.content;
|
|
552
|
+
if (!Array.isArray(content))
|
|
553
|
+
return;
|
|
554
|
+
for (const block of content) {
|
|
555
|
+
if (typeof block !== 'object' || block === null)
|
|
556
|
+
continue;
|
|
557
|
+
const image = block;
|
|
558
|
+
if (image.type !== 'image')
|
|
559
|
+
continue;
|
|
560
|
+
if (typeof image.data === 'string' && image.data !== '') {
|
|
561
|
+
this.#images.push({
|
|
562
|
+
data: image.data,
|
|
563
|
+
...(typeof image.mimeType === 'string' ? { mimeType: image.mimeType } : {}),
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/** The thread this wire is working in, once it has one. */
|
|
569
|
+
get threadId() {
|
|
570
|
+
return this.#threadId;
|
|
571
|
+
}
|
|
572
|
+
/** Authoritative turn id, adopted only after the start response agrees. */
|
|
573
|
+
get turnId() {
|
|
574
|
+
return this.#turnId;
|
|
575
|
+
}
|
|
576
|
+
/** Conservative request boundary, not inferred from progress or a known PID. */
|
|
577
|
+
get dispatch() { return this.#dispatch; }
|
|
578
|
+
/** Token usage attributable to this one delegation; never a zero guess. */
|
|
579
|
+
get usageReport() {
|
|
580
|
+
return this.#usage.report();
|
|
581
|
+
}
|
|
582
|
+
/** Account rate limits, kept separate from token usage. */
|
|
583
|
+
get rateLimitsReport() {
|
|
584
|
+
return {
|
|
585
|
+
observations: this.#rateLimitObservations,
|
|
586
|
+
...(this.#rateLimits === undefined ? {} : { latest: this.#rateLimits }),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/** The requested and effective configuration of the one start/resume. */
|
|
590
|
+
get configuration() {
|
|
591
|
+
return this.#configuration;
|
|
592
|
+
}
|
|
593
|
+
get turnConfiguration() {
|
|
594
|
+
return this.#turnConfiguration;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Submit the one text-only task and wait for this thread/turn's terminal
|
|
598
|
+
* notification.
|
|
599
|
+
*
|
|
600
|
+
* @param texts - already validated task text blocks.
|
|
601
|
+
* @param signal - local cancellation for the published run.
|
|
602
|
+
* @param effort - per-turn thinking level, if the caller chose one.
|
|
603
|
+
* @returns the selected output and its stop reason.
|
|
604
|
+
*/
|
|
605
|
+
async runTurn(texts, signal, effort, images = [], outputSchema, lifecycle) {
|
|
606
|
+
const completion = Promise.withResolvers();
|
|
607
|
+
this.#turnCompleted = completion;
|
|
608
|
+
const threadId = this.#threadId;
|
|
609
|
+
if (threadId === undefined || this.#configuration?.matched !== true) {
|
|
610
|
+
throw new Error(`${P}: cannot start a turn without verified thread configuration`);
|
|
611
|
+
}
|
|
612
|
+
// These are programmatic persistence barriers, never model text or human
|
|
613
|
+
// approval. Mark possible BEFORE writing: a crash after that ACK must stay
|
|
614
|
+
// ambiguous unless a durable terminal receipt can be reconciled.
|
|
615
|
+
if (lifecycle !== undefined) {
|
|
616
|
+
await this.#guarded(lifecycle('dispatch-intent'), signal);
|
|
617
|
+
this.#dispatch = 'possible';
|
|
618
|
+
await this.#guarded(lifecycle('dispatch-possible'), signal);
|
|
619
|
+
}
|
|
620
|
+
this.#dispatch = 'possible';
|
|
621
|
+
this.#usage.startTurnRequest();
|
|
622
|
+
this.#turnConfiguration = {
|
|
623
|
+
...(effort === undefined ? {} : { requestedEffort: effort }),
|
|
624
|
+
effectiveEffort: null, evidence: 'turn/start-request', accepted: false,
|
|
625
|
+
};
|
|
626
|
+
try {
|
|
627
|
+
const turn = object(object(await this.#guarded(this.#transport.request('turn/start', {
|
|
628
|
+
threadId,
|
|
629
|
+
// Images first, as `localImage`: the app-server enumerates its own
|
|
630
|
+
// input variants and rejects anything else, and a host path is the
|
|
631
|
+
// one form it takes without the bytes travelling at all.
|
|
632
|
+
input: [
|
|
633
|
+
...images.map((path) => ({ type: 'localImage', path })),
|
|
634
|
+
...texts.map((text) => ({ type: 'text', text, text_elements: [] })),
|
|
635
|
+
],
|
|
636
|
+
// ── this package's addition ──────────────────────────────────────
|
|
637
|
+
// The per-turn thinking level. The official adapter never sends it.
|
|
638
|
+
...effort === undefined ? {} : { effort },
|
|
639
|
+
// `outputSchema` belongs on the turn: the same field on thread/start
|
|
640
|
+
// is accepted and then silently ignored, measured — the answer came
|
|
641
|
+
// back as prose.
|
|
642
|
+
...outputSchema === undefined ? {} : { outputSchema },
|
|
643
|
+
}, { signal }), signal), 'turn/start response').turn, 'turn/start turn');
|
|
644
|
+
this.#commitTurnId(string(turn.id, 'turn/start turn id'));
|
|
645
|
+
this.#usage.beginTurn(string(turn.id, 'turn/start turn id'));
|
|
646
|
+
this.#turnConfiguration = { ...this.#turnConfiguration, accepted: true };
|
|
647
|
+
this.#dispatch = 'accepted';
|
|
648
|
+
if (lifecycle !== undefined)
|
|
649
|
+
await this.#guarded(lifecycle('turn-accepted'), signal);
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
this.#recordFailure({ stage: 'turn-start', category: 'unknown' });
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
let completed;
|
|
656
|
+
let terminal;
|
|
657
|
+
try {
|
|
658
|
+
completed = await this.#guarded(completion.promise, signal);
|
|
659
|
+
terminal = object(completed.params.turn, 'turn/completed turn');
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
this.#recordFailure({ stage: 'turn', category: 'unknown' });
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
const status = terminal.status;
|
|
666
|
+
if (status !== 'completed') {
|
|
667
|
+
const parsed = failureInfo(terminal);
|
|
668
|
+
this.#recordFailure(parsed.httpStatus === undefined
|
|
669
|
+
? { stage: 'turn', category: parsed.category }
|
|
670
|
+
: { stage: 'turn', category: parsed.category, httpStatus: parsed.httpStatus });
|
|
671
|
+
if (parsed.sandboxFailure === true) {
|
|
672
|
+
this.#recordDiagnostic('sandbox execution', 'failed', 'Codex reported a sandbox failure', completed.order);
|
|
673
|
+
}
|
|
674
|
+
if (parsed.maxTokens === true)
|
|
675
|
+
return { output: this.#collectOutput(), stopReason: 'max-tokens', images: this.images };
|
|
676
|
+
const detail = status === 'failed' ? `: ${parsed.category}` : '';
|
|
677
|
+
throw new Error(`${P}: Codex turn ended with status ${String(status)}${detail}`);
|
|
678
|
+
}
|
|
679
|
+
const output = this.#collectOutput();
|
|
680
|
+
if (output.length === 0) {
|
|
681
|
+
this.#recordFailure({ stage: 'turn', category: 'invalid-result' });
|
|
682
|
+
throw new Error(`${P}: Codex completed without a final answer`);
|
|
683
|
+
}
|
|
684
|
+
return { output, stopReason: 'completed', images: this.images };
|
|
685
|
+
}
|
|
686
|
+
/** The best non-commentary answer observed so far, preserving exact bytes. */
|
|
687
|
+
get partialOutput() {
|
|
688
|
+
return this.#collectOutput();
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Best-effort remote cancellation. Local settlement and process teardown stay
|
|
692
|
+
* authoritative when the child no longer accepts protocol requests.
|
|
693
|
+
*/
|
|
694
|
+
interrupt() {
|
|
695
|
+
if (this.#threadId === undefined || this.#turnId === undefined || this.#closed)
|
|
696
|
+
return;
|
|
697
|
+
this.#transport.request('turn/interrupt', { threadId: this.#threadId, turnId: this.#turnId }).catch(() => { });
|
|
698
|
+
}
|
|
699
|
+
/** The latest safe unattended permission fact observed for this run. */
|
|
700
|
+
collectDiagnostic() {
|
|
701
|
+
return this.#diagnostic;
|
|
702
|
+
}
|
|
703
|
+
/** The structured failure fact observed for this published turn. */
|
|
704
|
+
collectFailure() {
|
|
705
|
+
return this.#failure;
|
|
706
|
+
}
|
|
707
|
+
/** Detach listeners and fail outstanding requests. Idempotent. */
|
|
708
|
+
close() {
|
|
709
|
+
if (this.#closed)
|
|
710
|
+
return;
|
|
711
|
+
this.#closed = true;
|
|
712
|
+
this.#usage.close();
|
|
713
|
+
this.#transport.dispose(new Error(`${P}: app-server connection closed`));
|
|
714
|
+
}
|
|
715
|
+
/** The best non-commentary answer observed so far, preserving exact bytes. */
|
|
716
|
+
#collectOutput() {
|
|
717
|
+
// Latest final answer wins, then stable partial text (DSH 0.2.1's official
|
|
718
|
+
// adapter selects the same way), then an unphased message for compatibility.
|
|
719
|
+
const selected = this.#lastFinalAnswer ?? this.#lastPartialAnswer ?? this.#lastUnphasedAnswer;
|
|
720
|
+
return selected !== undefined && selected.trim().length > 0 ? [{ type: 'text', text: selected }] : [];
|
|
721
|
+
}
|
|
722
|
+
/** Race one operation against the connection's fatal failure. */
|
|
723
|
+
async #guarded(pending, signal) {
|
|
724
|
+
return raceAbort(Promise.race([this.#fatal.promise, pending]), signal);
|
|
725
|
+
}
|
|
726
|
+
/** Poison the connection: every guarded operation fails from here on. */
|
|
727
|
+
#fail(error) {
|
|
728
|
+
this.#fatal.reject(error);
|
|
729
|
+
}
|
|
730
|
+
#onInputError = (error) => { this.#fail(error); };
|
|
731
|
+
#onOutputError = (error) => { this.#fail(error); };
|
|
732
|
+
#onInputEnd = () => {
|
|
733
|
+
this.#inputEnded = true;
|
|
734
|
+
this.#fail(new Error(`${P}: app-server protocol stream closed`));
|
|
735
|
+
};
|
|
736
|
+
/** Record a turn id observed before `turn/start` returned. */
|
|
737
|
+
#observePendingTurnId(id) {
|
|
738
|
+
if (this.#turnCompleted === undefined)
|
|
739
|
+
throw new Error(`${P}: app-server referenced a turn before turn/start`);
|
|
740
|
+
if (this.#pendingTurnId !== undefined && this.#pendingTurnId !== id) {
|
|
741
|
+
throw new Error(`${P}: app-server referenced conflicting turns`);
|
|
742
|
+
}
|
|
743
|
+
this.#pendingTurnId = id;
|
|
744
|
+
}
|
|
745
|
+
/** Adopt the authoritative turn id and replay anything that arrived first. */
|
|
746
|
+
#commitTurnId(id) {
|
|
747
|
+
if (this.#pendingTurnId !== undefined && this.#pendingTurnId !== id) {
|
|
748
|
+
throw new Error(`${P}: turn/start response did not match the active turn`);
|
|
749
|
+
}
|
|
750
|
+
this.#turnId = id;
|
|
751
|
+
const pendingDiagnostic = this.#pendingDiagnostic;
|
|
752
|
+
this.#pendingDiagnostic = undefined;
|
|
753
|
+
if (pendingDiagnostic !== undefined) {
|
|
754
|
+
this.#recordDiagnostic(pendingDiagnostic.request, pendingDiagnostic.decision, pendingDiagnostic.reason, pendingDiagnostic.order);
|
|
755
|
+
}
|
|
756
|
+
const notifications = this.#earlyTurnNotifications.splice(0);
|
|
757
|
+
for (const notification of notifications)
|
|
758
|
+
this.#handleNotification(notification.method, notification.params, notification.order);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Validate a request's thread and turn association.
|
|
762
|
+
*
|
|
763
|
+
* @param params - the server request's params.
|
|
764
|
+
* @param nullableTurn - whether a null turn id is acceptable.
|
|
765
|
+
* @returns `true` when the matching turn is still provisional.
|
|
766
|
+
*/
|
|
767
|
+
#validateRunIds(params, nullableTurn = false) {
|
|
768
|
+
if (params.threadId !== this.#threadId)
|
|
769
|
+
throw new Error(`${P}: app-server request referenced another thread`);
|
|
770
|
+
if (nullableTurn && params.turnId === null)
|
|
771
|
+
return false;
|
|
772
|
+
const id = string(params.turnId, 'server request turn id');
|
|
773
|
+
if (this.#turnId === undefined) {
|
|
774
|
+
this.#observePendingTurnId(id);
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
if (id !== this.#turnId)
|
|
778
|
+
throw new Error(`${P}: app-server request referenced another turn`);
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
/** Record a decision, deferring when its turn is not authoritative yet. */
|
|
782
|
+
#recordRequestDiagnostic(provisional, request, decision, reason) {
|
|
783
|
+
const order = this.#nextObservationOrder();
|
|
784
|
+
if (provisional) {
|
|
785
|
+
this.#pendingDiagnostic = { order, request, decision, reason };
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
this.#recordDiagnostic(request, decision, reason, order);
|
|
789
|
+
}
|
|
790
|
+
/** Keep the newest diagnostic. */
|
|
791
|
+
#recordDiagnostic(request, decision, reason, order = this.#nextObservationOrder()) {
|
|
792
|
+
if (order < this.#diagnosticOrder)
|
|
793
|
+
return;
|
|
794
|
+
this.#diagnosticOrder = order;
|
|
795
|
+
this.#diagnostic = `Codex unattended decision (mode: ${this.#permissionMode}; request: ${request}; decision: ${decision}): ${reason}`;
|
|
796
|
+
}
|
|
797
|
+
#recordFailure(facts) {
|
|
798
|
+
this.#failure = facts;
|
|
799
|
+
}
|
|
800
|
+
#nextObservationOrder() {
|
|
801
|
+
this.#observationOrder += 1;
|
|
802
|
+
return this.#observationOrder;
|
|
803
|
+
}
|
|
804
|
+
/** Note an item Codex declined, which is a permission fact worth reporting. */
|
|
805
|
+
#recordDeclinedItem(item, order) {
|
|
806
|
+
if (item.type === 'commandExecution' && item.status === 'declined') {
|
|
807
|
+
this.#recordDiagnostic('command execution', 'declined', 'Codex declined the command under the selected permission mode', order);
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
if (item.type === 'fileChange' && item.status === 'declined') {
|
|
811
|
+
this.#recordDiagnostic('file change', 'declined', 'Codex declined the file change under the selected permission mode', order);
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
/** Answer every interactive request without a human. */
|
|
817
|
+
#handleServerRequest(method, params) {
|
|
818
|
+
try {
|
|
819
|
+
const fields = (params ?? {});
|
|
820
|
+
switch (method) {
|
|
821
|
+
case 'item/commandExecution/requestApproval': {
|
|
822
|
+
const provisional = this.#validateRunIds(fields);
|
|
823
|
+
const decision = unattendedDecision(fields);
|
|
824
|
+
this.#recordRequestDiagnostic(provisional, 'command approval', decision === 'cancel' ? 'cancelled' : 'declined', 'the provider does not grant interactive approval');
|
|
825
|
+
return Promise.resolve({ decision });
|
|
826
|
+
}
|
|
827
|
+
case 'item/fileChange/requestApproval': {
|
|
828
|
+
const provisional = this.#validateRunIds(fields);
|
|
829
|
+
const decision = unattendedDecision(fields);
|
|
830
|
+
this.#recordRequestDiagnostic(provisional, 'file approval', decision === 'cancel' ? 'cancelled' : 'declined', 'the provider does not grant interactive approval');
|
|
831
|
+
return Promise.resolve({ decision });
|
|
832
|
+
}
|
|
833
|
+
case 'item/permissions/requestApproval':
|
|
834
|
+
this.#recordRequestDiagnostic(this.#validateRunIds(fields), 'permission grant', 'denied', 'the provider grants no additional turn permissions');
|
|
835
|
+
return Promise.resolve({ permissions: {}, scope: 'turn' });
|
|
836
|
+
case 'item/tool/requestUserInput':
|
|
837
|
+
this.#recordRequestDiagnostic(this.#validateRunIds(fields), 'user input', 'empty response', 'the provider does not collect interactive answers');
|
|
838
|
+
return Promise.resolve({ answers: {} });
|
|
839
|
+
case 'mcpServer/elicitation/request':
|
|
840
|
+
this.#recordRequestDiagnostic(this.#validateRunIds(fields, true), 'MCP elicitation', 'declined', 'the provider does not collect interactive MCP input');
|
|
841
|
+
return Promise.resolve({ action: 'decline', content: null, _meta: null });
|
|
842
|
+
default:
|
|
843
|
+
throw new Error(`${P}: unsupported app-server request ${JSON.stringify(method)}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
catch (error) {
|
|
847
|
+
const normalized = thrown(error);
|
|
848
|
+
this.#fail(normalized);
|
|
849
|
+
return Promise.reject(normalized);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/** Replay usage notifications that raced the start/resume response. */
|
|
853
|
+
#flushEarlyUsage() {
|
|
854
|
+
if (this.#threadId === undefined)
|
|
855
|
+
return;
|
|
856
|
+
if (this.#earlyUsageOverflow)
|
|
857
|
+
this.#usage.observeInvalid();
|
|
858
|
+
const pending = this.#earlyUsageNotifications.splice(0);
|
|
859
|
+
for (const notification of pending) {
|
|
860
|
+
if (typeof notification.threadId !== 'string' || notification.threadId === '') {
|
|
861
|
+
this.#usage.observeInvalid();
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (notification.threadId !== this.#threadId)
|
|
865
|
+
continue;
|
|
866
|
+
const tokenUsage = notification.tokenUsage;
|
|
867
|
+
if (typeof tokenUsage !== 'object' || tokenUsage === null || Array.isArray(tokenUsage)) {
|
|
868
|
+
this.#usage.observeInvalid();
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
this.#usage.observe(notification);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/** Route one notification, buffering anything that precedes the turn id. */
|
|
875
|
+
#handleNotification(method, params, order) {
|
|
876
|
+
const fields = (params ?? {});
|
|
877
|
+
if (method === 'thread/tokenUsage/updated') {
|
|
878
|
+
if (this.#threadId === undefined) {
|
|
879
|
+
if (this.#earlyUsageNotifications.length < 128) {
|
|
880
|
+
this.#earlyUsageNotifications.push({
|
|
881
|
+
threadId: fields.threadId,
|
|
882
|
+
turnId: fields.turnId,
|
|
883
|
+
tokenUsage: fields.tokenUsage,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
else
|
|
887
|
+
this.#earlyUsageOverflow = true;
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (typeof fields.threadId !== 'string' || fields.threadId === '') {
|
|
891
|
+
this.#usage.observeInvalid();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (fields.threadId !== this.#threadId)
|
|
895
|
+
return;
|
|
896
|
+
const tokenUsage = fields.tokenUsage;
|
|
897
|
+
if (typeof tokenUsage !== 'object' || tokenUsage === null || Array.isArray(tokenUsage)) {
|
|
898
|
+
this.#usage.observeInvalid();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
this.#usage.observe({
|
|
902
|
+
threadId: fields.threadId,
|
|
903
|
+
turnId: fields.turnId,
|
|
904
|
+
tokenUsage,
|
|
905
|
+
});
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (method === 'account/rateLimits/updated') {
|
|
909
|
+
const rateLimits = fields.rateLimits;
|
|
910
|
+
if (typeof rateLimits !== 'object' || rateLimits === null || Array.isArray(rateLimits))
|
|
911
|
+
return;
|
|
912
|
+
this.#rateLimits = rateLimits;
|
|
913
|
+
this.#rateLimitObservations += 1;
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (method === 'turn/started') {
|
|
917
|
+
if (string(fields.threadId, 'turn/started thread id') !== this.#threadId)
|
|
918
|
+
return;
|
|
919
|
+
const turn = object(fields.turn, 'turn/started turn');
|
|
920
|
+
if (this.#turnCompleted !== undefined && this.#turnId === undefined) {
|
|
921
|
+
this.#observePendingTurnId(string(turn.id, 'turn/started turn id'));
|
|
922
|
+
}
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (method === 'item/completed') {
|
|
926
|
+
if (string(fields.threadId, 'item/completed thread id') !== this.#threadId)
|
|
927
|
+
return;
|
|
928
|
+
const id = string(fields.turnId, 'item/completed turn id');
|
|
929
|
+
if (this.#turnId === undefined) {
|
|
930
|
+
if (this.#turnCompleted !== undefined) {
|
|
931
|
+
this.#observePendingTurnId(id);
|
|
932
|
+
this.#earlyTurnNotifications.push({ method, params, order: this.#nextObservationOrder() });
|
|
933
|
+
}
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (id !== this.#turnId)
|
|
937
|
+
return;
|
|
938
|
+
const item = object(fields.item, 'item/completed item');
|
|
939
|
+
if (this.#recordDeclinedItem(item, order))
|
|
940
|
+
return;
|
|
941
|
+
this.#recordImages(item);
|
|
942
|
+
if (item.type !== 'agentMessage') {
|
|
943
|
+
// Commands, file changes, and searches: the interesting part of a long run.
|
|
944
|
+
this.#progress(describeItem(item));
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const text = typeof item.text === 'string'
|
|
948
|
+
? item.text
|
|
949
|
+
: (() => { throw new Error(`${P}: app-server returned an invalid agent message`); })();
|
|
950
|
+
if (item.phase === 'final_answer')
|
|
951
|
+
this.#lastFinalAnswer = text;
|
|
952
|
+
else if (item.phase === 'partial_answer')
|
|
953
|
+
this.#lastPartialAnswer = text;
|
|
954
|
+
else if (item.phase === null)
|
|
955
|
+
this.#lastUnphasedAnswer = text;
|
|
956
|
+
else if (item.phase === 'commentary')
|
|
957
|
+
this.#progress(text);
|
|
958
|
+
else {
|
|
959
|
+
throw new Error(`${P}: app-server returned an unknown agent message phase ${JSON.stringify(item.phase)}`);
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (method !== 'turn/completed')
|
|
964
|
+
return;
|
|
965
|
+
if (string(fields.threadId, 'turn/completed thread id') !== this.#threadId)
|
|
966
|
+
return;
|
|
967
|
+
const turn = object(fields.turn, 'turn/completed turn');
|
|
968
|
+
const id = string(turn.id, 'turn/completed turn id');
|
|
969
|
+
const turnCompleted = this.#turnCompleted;
|
|
970
|
+
if (turnCompleted === undefined)
|
|
971
|
+
return;
|
|
972
|
+
if (this.#turnId === undefined) {
|
|
973
|
+
this.#observePendingTurnId(id);
|
|
974
|
+
this.#earlyTurnNotifications.push({ method, params, order: this.#nextObservationOrder() });
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (id !== this.#turnId)
|
|
978
|
+
return;
|
|
979
|
+
this.#terminalObserved = true;
|
|
980
|
+
if (!['completed', 'interrupted', 'failed'].includes(String(turn.status))) {
|
|
981
|
+
throw new Error(`${P}: app-server returned invalid terminal turn status ${String(turn.status)}`);
|
|
982
|
+
}
|
|
983
|
+
turnCompleted.resolve({ params: params, order: order ?? this.#nextObservationOrder() });
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
//# sourceMappingURL=wire.js.map
|