@deepseek-ai/dsh-subagent-codex 0.1.0-rc.7 → 0.1.0-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +62 -17
- package/README.zh.md +62 -17
- package/cordis.patch.yml +5 -0
- package/lib/index.js +482 -91
- package/lib/types/index.d.ts +11 -6
- package/lib/types/run.d.ts +17 -8
- package/lib/types/wire.d.ts +49 -1
- package/package.json +25 -20
package/lib/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import z from "@deepseek-ai/schemastery";
|
|
2
3
|
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
3
4
|
import { NO_START_CAPABILITIES, assertPositiveFinite, resolveChildCwd, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
5
8
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
6
9
|
import { JsonRpcLineTransport } from "@deepseek-ai/dsh-sdk-protocol";
|
|
7
10
|
//#region lib/types/wire.js
|
|
@@ -13,6 +16,37 @@ import { JsonRpcLineTransport } from "@deepseek-ai/dsh-sdk-protocol";
|
|
|
13
16
|
*
|
|
14
17
|
* @module @deepseek-ai/dsh-subagent-codex/wire
|
|
15
18
|
*/
|
|
19
|
+
const THREAD_PERMISSION_PARAMS = {
|
|
20
|
+
never: { approvalPolicy: "never" },
|
|
21
|
+
"approve-for-me": {
|
|
22
|
+
approvalPolicy: "on-request",
|
|
23
|
+
approvalsReviewer: "auto_review",
|
|
24
|
+
sandbox: "workspace-write"
|
|
25
|
+
},
|
|
26
|
+
"dangerously-bypass-approvals-and-sandbox": {
|
|
27
|
+
approvalPolicy: "never",
|
|
28
|
+
sandbox: "danger-full-access"
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const STDERR_PERMISSION_SIGNATURES = [{
|
|
32
|
+
text: "approval policy is Never; reject command",
|
|
33
|
+
request: "command execution",
|
|
34
|
+
decision: "denied",
|
|
35
|
+
reason: "Codex rejected an escalation because the selected policy never asks for approval"
|
|
36
|
+
}, {
|
|
37
|
+
text: "recorded sandbox violation:",
|
|
38
|
+
request: "sandbox execution",
|
|
39
|
+
decision: "failed",
|
|
40
|
+
reason: "Codex reported a sandbox violation"
|
|
41
|
+
}];
|
|
42
|
+
const STDERR_SIGNATURE_TAIL_CHARS = Math.max(...STDERR_PERMISSION_SIGNATURES.map((signature) => signature.text.length)) - 1;
|
|
43
|
+
function stderrSignatureTail(value) {
|
|
44
|
+
for (let length = Math.min(STDERR_SIGNATURE_TAIL_CHARS, value.length); length > 0; length -= 1) {
|
|
45
|
+
const tail = value.slice(-length);
|
|
46
|
+
if (STDERR_PERMISSION_SIGNATURES.some((signature) => tail.length < signature.text.length && signature.text.startsWith(tail))) return tail;
|
|
47
|
+
}
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
16
50
|
function object(value, label) {
|
|
17
51
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`subagent-codex: app-server returned invalid ${label}`);
|
|
18
52
|
return value;
|
|
@@ -30,10 +64,54 @@ function unattendedDecision(params) {
|
|
|
30
64
|
}
|
|
31
65
|
throw new Error("subagent-codex: app-server offered no unattended approval decision");
|
|
32
66
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
67
|
+
function numericHttpStatus(value) {
|
|
68
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 65535 ? value : void 0;
|
|
69
|
+
}
|
|
70
|
+
function objectFailureInfo(value) {
|
|
71
|
+
const keys = Object.keys(value);
|
|
72
|
+
const category = keys[0];
|
|
73
|
+
if (keys.length !== 1 || category === void 0) return { category: "unknown" };
|
|
74
|
+
const detail = value[category];
|
|
75
|
+
if (detail === null || typeof detail !== "object" || Array.isArray(detail)) return { category: "unknown" };
|
|
76
|
+
const fields = detail;
|
|
77
|
+
switch (category) {
|
|
78
|
+
case "httpConnectionFailed":
|
|
79
|
+
case "responseStreamConnectionFailed":
|
|
80
|
+
case "responseStreamDisconnected":
|
|
81
|
+
case "responseTooManyFailedAttempts": {
|
|
82
|
+
const httpStatus = numericHttpStatus(fields.httpStatusCode);
|
|
83
|
+
return httpStatus === void 0 ? { category } : {
|
|
84
|
+
category,
|
|
85
|
+
httpStatus
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
case "activeTurnNotSteerable": return { category };
|
|
89
|
+
default: return { category: "unknown" };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function failureInfo(turn) {
|
|
93
|
+
if (turn.status !== "failed") return { category: "unknown" };
|
|
35
94
|
const error = turn.error;
|
|
36
|
-
|
|
95
|
+
if (error === null || typeof error !== "object" || Array.isArray(error)) return { category: "unknown" };
|
|
96
|
+
const info = error.codexErrorInfo;
|
|
97
|
+
if (typeof info === "string") switch (info) {
|
|
98
|
+
case "contextWindowExceeded":
|
|
99
|
+
case "sessionBudgetExceeded":
|
|
100
|
+
case "usageLimitExceeded":
|
|
101
|
+
case "serverOverloaded":
|
|
102
|
+
case "cyberPolicy":
|
|
103
|
+
case "internalServerError":
|
|
104
|
+
case "unauthorized":
|
|
105
|
+
case "badRequest":
|
|
106
|
+
case "threadRollbackFailed":
|
|
107
|
+
case "sandboxError":
|
|
108
|
+
case "other": return { category: info };
|
|
109
|
+
default: return { category: "unknown" };
|
|
110
|
+
}
|
|
111
|
+
return info !== null && typeof info === "object" && !Array.isArray(info) ? objectFailureInfo(info) : { category: "unknown" };
|
|
112
|
+
}
|
|
113
|
+
function unattendedDiagnostic(mode, request, decision, reason) {
|
|
114
|
+
return `Codex unattended decision (mode: ${mode}; request: ${request}; decision: ${decision}): ${reason}`;
|
|
37
115
|
}
|
|
38
116
|
function thrown$1(value) {
|
|
39
117
|
/* v8 ignore next -- typed protocol and stream failures reject with Error. */
|
|
@@ -69,6 +147,7 @@ async function raceAbort(pending, signal) {
|
|
|
69
147
|
*/
|
|
70
148
|
var CodexAppServerWire = class {
|
|
71
149
|
input;
|
|
150
|
+
permissionMode;
|
|
72
151
|
transport;
|
|
73
152
|
fatal = Promise.withResolvers();
|
|
74
153
|
threadId;
|
|
@@ -78,9 +157,18 @@ var CodexAppServerWire = class {
|
|
|
78
157
|
earlyTurnNotifications = [];
|
|
79
158
|
lastFinalAnswer;
|
|
80
159
|
lastUnphasedAnswer;
|
|
160
|
+
diagnostic;
|
|
161
|
+
failure;
|
|
162
|
+
diagnosticOrder = 0;
|
|
163
|
+
observationOrder = 0;
|
|
164
|
+
pendingDiagnostic;
|
|
165
|
+
stderrTail = "";
|
|
166
|
+
inputEnded = false;
|
|
167
|
+
terminalObserved = false;
|
|
81
168
|
closed = false;
|
|
82
|
-
constructor(input, output) {
|
|
169
|
+
constructor(input, output, permissionMode) {
|
|
83
170
|
this.input = input;
|
|
171
|
+
this.permissionMode = permissionMode;
|
|
84
172
|
this.transport = new JsonRpcLineTransport(input, output);
|
|
85
173
|
this.fatal.promise.catch(() => {});
|
|
86
174
|
this.transport.onRequest((method, params) => this.handleServerRequest(method, params));
|
|
@@ -100,6 +188,13 @@ var CodexAppServerWire = class {
|
|
|
100
188
|
this.transport.start();
|
|
101
189
|
}
|
|
102
190
|
/**
|
|
191
|
+
* Whether protocol output ended before a terminal turn notification.
|
|
192
|
+
* @returns `true` only for an early protocol close without a terminal turn.
|
|
193
|
+
*/
|
|
194
|
+
endedBeforeTerminal() {
|
|
195
|
+
return this.inputEnded && !this.terminalObserved;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
103
198
|
* Perform the required app-server initialize/initialized handshake.
|
|
104
199
|
* @param signal - unpublished-start cancellation.
|
|
105
200
|
*/
|
|
@@ -126,7 +221,8 @@ var CodexAppServerWire = class {
|
|
|
126
221
|
async startThread(cwd, signal) {
|
|
127
222
|
const thread = object(object(await this.guarded(this.transport.request("thread/start", {
|
|
128
223
|
cwd,
|
|
129
|
-
ephemeral: true
|
|
224
|
+
ephemeral: true,
|
|
225
|
+
...THREAD_PERMISSION_PARAMS[this.permissionMode]
|
|
130
226
|
}, signal), signal), "thread/start response").thread, "thread/start thread");
|
|
131
227
|
const id = string(thread.id, "thread/start thread id");
|
|
132
228
|
if (thread.ephemeral !== true) throw new Error("subagent-codex: app-server did not create an ephemeral thread");
|
|
@@ -143,27 +239,62 @@ var CodexAppServerWire = class {
|
|
|
143
239
|
const completion = Promise.withResolvers();
|
|
144
240
|
this.turnCompleted = completion;
|
|
145
241
|
const threadId = this.threadId;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
242
|
+
try {
|
|
243
|
+
const turn = object(object(await this.guarded(this.transport.request("turn/start", {
|
|
244
|
+
threadId,
|
|
245
|
+
input: texts.map((text) => ({
|
|
246
|
+
type: "text",
|
|
247
|
+
text,
|
|
248
|
+
text_elements: []
|
|
249
|
+
}))
|
|
250
|
+
}, signal), signal), "turn/start response").turn, "turn/start turn");
|
|
251
|
+
this.commitTurnId(string(turn.id, "turn/start turn id"));
|
|
252
|
+
} catch (error) {
|
|
253
|
+
this.recordFailure({
|
|
254
|
+
stage: "turn-start",
|
|
255
|
+
category: "unknown"
|
|
256
|
+
});
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
let completed;
|
|
260
|
+
let terminal;
|
|
261
|
+
try {
|
|
262
|
+
completed = await this.guarded(completion.promise, signal);
|
|
263
|
+
terminal = object(completed.params.turn, "turn/completed turn");
|
|
264
|
+
} catch (error) {
|
|
265
|
+
this.recordFailure({
|
|
266
|
+
stage: "turn",
|
|
267
|
+
category: "unknown"
|
|
268
|
+
});
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
156
271
|
const status = terminal.status;
|
|
157
|
-
if (isContextWindowExceeded(terminal)) return {
|
|
158
|
-
output: this.collectOutput(),
|
|
159
|
-
stopReason: "max-tokens"
|
|
160
|
-
};
|
|
161
272
|
if (status !== "completed") {
|
|
162
|
-
const
|
|
273
|
+
const parsed = failureInfo(terminal);
|
|
274
|
+
this.recordFailure(parsed.httpStatus === void 0 ? {
|
|
275
|
+
stage: "turn",
|
|
276
|
+
category: parsed.category
|
|
277
|
+
} : {
|
|
278
|
+
stage: "turn",
|
|
279
|
+
category: parsed.category,
|
|
280
|
+
httpStatus: parsed.httpStatus
|
|
281
|
+
});
|
|
282
|
+
if (parsed.category === "sandboxError") this.recordDiagnostic("sandbox execution", "failed", "Codex reported a sandbox failure", completed.order);
|
|
283
|
+
if (parsed.category === "contextWindowExceeded") return {
|
|
284
|
+
output: this.collectOutput(),
|
|
285
|
+
stopReason: "max-tokens"
|
|
286
|
+
};
|
|
287
|
+
const detail = status === "failed" ? `: ${parsed.category}` : "";
|
|
163
288
|
throw new Error(`subagent-codex: Codex turn ended with status ${String(status)}${detail}`);
|
|
164
289
|
}
|
|
165
290
|
const output = this.collectOutput();
|
|
166
|
-
if (output.length === 0)
|
|
291
|
+
if (output.length === 0) {
|
|
292
|
+
this.recordFailure({
|
|
293
|
+
stage: "turn",
|
|
294
|
+
category: "unknown"
|
|
295
|
+
});
|
|
296
|
+
throw new Error("subagent-codex: Codex completed without a final answer");
|
|
297
|
+
}
|
|
167
298
|
return {
|
|
168
299
|
output,
|
|
169
300
|
stopReason: "completed"
|
|
@@ -191,6 +322,40 @@ var CodexAppServerWire = class {
|
|
|
191
322
|
text: selected
|
|
192
323
|
}] : [];
|
|
193
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* The latest safe unattended permission fact observed for this run.
|
|
327
|
+
* @returns provider-authored diagnostic text, when one was observed.
|
|
328
|
+
*/
|
|
329
|
+
collectDiagnostic() {
|
|
330
|
+
return this.diagnostic;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The structured failure fact observed for this published turn.
|
|
334
|
+
* Call only after a non-completed return or rejection from {@link runTurn}.
|
|
335
|
+
* @returns the fixed stage/category pair and optional HTTP status.
|
|
336
|
+
*/
|
|
337
|
+
collectFailure() {
|
|
338
|
+
return this.failure;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Observe product stderr while retaining only enough tail to recognize fixed
|
|
342
|
+
* permission signatures. The raw text is never copied into the diagnostic.
|
|
343
|
+
* @param chunk - one decoded stderr chunk already forwarded to the host.
|
|
344
|
+
*/
|
|
345
|
+
observeStderr(chunk) {
|
|
346
|
+
const observed = `${this.stderrTail}${chunk}`;
|
|
347
|
+
let latestIndex = -1;
|
|
348
|
+
let latest;
|
|
349
|
+
for (const signature of STDERR_PERMISSION_SIGNATURES) {
|
|
350
|
+
const index = observed.lastIndexOf(signature.text);
|
|
351
|
+
if (index > latestIndex) {
|
|
352
|
+
latestIndex = index;
|
|
353
|
+
latest = signature;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (latest !== void 0) this.recordDiagnostic(latest.request, latest.decision, latest.reason);
|
|
357
|
+
this.stderrTail = stderrSignatureTail(observed);
|
|
358
|
+
}
|
|
194
359
|
/** Detach JSON-RPC listeners and reject outstanding requests. Idempotent. */
|
|
195
360
|
close() {
|
|
196
361
|
if (this.closed) return;
|
|
@@ -211,6 +376,7 @@ var CodexAppServerWire = class {
|
|
|
211
376
|
this.fail(error);
|
|
212
377
|
};
|
|
213
378
|
onInputEnd = () => {
|
|
379
|
+
this.inputEnded = true;
|
|
214
380
|
this.fail(/* @__PURE__ */ new Error("subagent-codex: app-server protocol stream closed"));
|
|
215
381
|
};
|
|
216
382
|
observePendingTurnId(id) {
|
|
@@ -221,37 +387,90 @@ var CodexAppServerWire = class {
|
|
|
221
387
|
commitTurnId(id) {
|
|
222
388
|
if (this.pendingTurnId !== void 0 && this.pendingTurnId !== id) throw new Error("subagent-codex: turn/start response did not match the active turn");
|
|
223
389
|
this.turnId = id;
|
|
390
|
+
const pendingDiagnostic = this.pendingDiagnostic;
|
|
391
|
+
this.pendingDiagnostic = void 0;
|
|
392
|
+
if (pendingDiagnostic !== void 0) this.recordDiagnostic(pendingDiagnostic.request, pendingDiagnostic.decision, pendingDiagnostic.reason, pendingDiagnostic.order);
|
|
224
393
|
const notifications = this.earlyTurnNotifications.splice(0);
|
|
225
|
-
for (const notification of notifications) this.handleNotification(notification.method, notification.params);
|
|
394
|
+
for (const notification of notifications) this.handleNotification(notification.method, notification.params, notification.order);
|
|
226
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Validate the request's thread and turn association.
|
|
398
|
+
* @returns `true` when the matching turn is still provisional, so the caller
|
|
399
|
+
* defers its diagnostic until `commitTurnId()`.
|
|
400
|
+
*/
|
|
227
401
|
validateRunIds(params, nullableTurn = false) {
|
|
228
402
|
if (params.threadId !== this.threadId) throw new Error("subagent-codex: app-server request referenced another thread");
|
|
229
|
-
if (nullableTurn && params.turnId === null) return;
|
|
403
|
+
if (nullableTurn && params.turnId === null) return false;
|
|
230
404
|
const id = string(params.turnId, "server request turn id");
|
|
231
405
|
if (this.turnId === void 0) {
|
|
232
406
|
this.observePendingTurnId(id);
|
|
233
|
-
return;
|
|
407
|
+
return true;
|
|
234
408
|
}
|
|
235
409
|
if (id !== this.turnId) throw new Error("subagent-codex: app-server request referenced another turn");
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
recordRequestDiagnostic(provisional, request, decision, reason) {
|
|
413
|
+
const order = this.nextObservationOrder();
|
|
414
|
+
if (provisional) {
|
|
415
|
+
this.pendingDiagnostic = {
|
|
416
|
+
order,
|
|
417
|
+
request,
|
|
418
|
+
decision,
|
|
419
|
+
reason
|
|
420
|
+
};
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this.recordDiagnostic(request, decision, reason, order);
|
|
424
|
+
}
|
|
425
|
+
recordDiagnostic(request, decision, reason, order = this.nextObservationOrder()) {
|
|
426
|
+
if (order < this.diagnosticOrder) return;
|
|
427
|
+
this.diagnosticOrder = order;
|
|
428
|
+
this.diagnostic = unattendedDiagnostic(this.permissionMode, request, decision, reason);
|
|
429
|
+
}
|
|
430
|
+
recordFailure(facts) {
|
|
431
|
+
this.failure = facts;
|
|
432
|
+
}
|
|
433
|
+
nextObservationOrder() {
|
|
434
|
+
this.observationOrder += 1;
|
|
435
|
+
return this.observationOrder;
|
|
436
|
+
}
|
|
437
|
+
recordDeclinedItem(item, order) {
|
|
438
|
+
if (item.type === "commandExecution" && item.status === "declined") {
|
|
439
|
+
this.recordDiagnostic("command execution", "declined", "Codex declined the command under the selected permission mode", order);
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
if (item.type === "fileChange" && item.status === "declined") {
|
|
443
|
+
this.recordDiagnostic("file change", "declined", "Codex declined the file change under the selected permission mode", order);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
236
447
|
}
|
|
237
448
|
handleServerRequest(method, params) {
|
|
238
449
|
try {
|
|
239
450
|
switch (method) {
|
|
240
|
-
case "item/commandExecution/requestApproval":
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
451
|
+
case "item/commandExecution/requestApproval": {
|
|
452
|
+
const provisional = this.validateRunIds(params);
|
|
453
|
+
const decision = unattendedDecision(params);
|
|
454
|
+
this.recordRequestDiagnostic(provisional, "command approval", decision === "cancel" ? "cancelled" : "declined", "the provider does not grant interactive approval");
|
|
455
|
+
return Promise.resolve({ decision });
|
|
456
|
+
}
|
|
457
|
+
case "item/fileChange/requestApproval": {
|
|
458
|
+
const provisional = this.validateRunIds(params);
|
|
459
|
+
const decision = unattendedDecision(params);
|
|
460
|
+
this.recordRequestDiagnostic(provisional, "file approval", decision === "cancel" ? "cancelled" : "declined", "the provider does not grant interactive approval");
|
|
461
|
+
return Promise.resolve({ decision });
|
|
462
|
+
}
|
|
244
463
|
case "item/permissions/requestApproval":
|
|
245
|
-
this.validateRunIds(params);
|
|
464
|
+
this.recordRequestDiagnostic(this.validateRunIds(params), "permission grant", "denied", "the provider grants no additional turn permissions");
|
|
246
465
|
return Promise.resolve({
|
|
247
466
|
permissions: {},
|
|
248
467
|
scope: "turn"
|
|
249
468
|
});
|
|
250
469
|
case "item/tool/requestUserInput":
|
|
251
|
-
this.validateRunIds(params);
|
|
470
|
+
this.recordRequestDiagnostic(this.validateRunIds(params), "user input", "empty response", "the provider does not collect interactive answers");
|
|
252
471
|
return Promise.resolve({ answers: {} });
|
|
253
472
|
case "mcpServer/elicitation/request":
|
|
254
|
-
this.validateRunIds(params, true);
|
|
473
|
+
this.recordRequestDiagnostic(this.validateRunIds(params, true), "MCP elicitation", "declined", "the provider does not collect interactive MCP input");
|
|
255
474
|
return Promise.resolve({
|
|
256
475
|
action: "decline",
|
|
257
476
|
content: null,
|
|
@@ -265,7 +484,7 @@ var CodexAppServerWire = class {
|
|
|
265
484
|
return Promise.reject(normalized);
|
|
266
485
|
}
|
|
267
486
|
}
|
|
268
|
-
handleNotification(method, params) {
|
|
487
|
+
handleNotification(method, params, order) {
|
|
269
488
|
if (method === "turn/started") {
|
|
270
489
|
if (string(params.threadId, "turn/started thread id") !== this.threadId) return;
|
|
271
490
|
const turn = object(params.turn, "turn/started turn");
|
|
@@ -280,13 +499,15 @@ var CodexAppServerWire = class {
|
|
|
280
499
|
this.observePendingTurnId(id);
|
|
281
500
|
this.earlyTurnNotifications.push({
|
|
282
501
|
method,
|
|
283
|
-
params
|
|
502
|
+
params,
|
|
503
|
+
order: this.nextObservationOrder()
|
|
284
504
|
});
|
|
285
505
|
}
|
|
286
506
|
return;
|
|
287
507
|
}
|
|
288
508
|
if (id !== this.turnId) return;
|
|
289
509
|
const item = object(params.item, "item/completed item");
|
|
510
|
+
if (this.recordDeclinedItem(item, order)) return;
|
|
290
511
|
if (item.type !== "agentMessage") return;
|
|
291
512
|
const text = typeof item.text === "string" ? item.text : (() => {
|
|
292
513
|
throw new Error("subagent-codex: app-server returned an invalid agent message");
|
|
@@ -306,17 +527,22 @@ var CodexAppServerWire = class {
|
|
|
306
527
|
this.observePendingTurnId(id);
|
|
307
528
|
this.earlyTurnNotifications.push({
|
|
308
529
|
method,
|
|
309
|
-
params
|
|
530
|
+
params,
|
|
531
|
+
order: this.nextObservationOrder()
|
|
310
532
|
});
|
|
311
533
|
return;
|
|
312
534
|
}
|
|
313
535
|
if (id !== this.turnId) return;
|
|
536
|
+
this.terminalObserved = true;
|
|
314
537
|
if (![
|
|
315
538
|
"completed",
|
|
316
539
|
"interrupted",
|
|
317
540
|
"failed"
|
|
318
541
|
].includes(String(turn.status))) throw new Error(`subagent-codex: app-server returned invalid terminal turn status ${String(turn.status)}`);
|
|
319
|
-
turnCompleted.resolve(
|
|
542
|
+
turnCompleted.resolve({
|
|
543
|
+
params,
|
|
544
|
+
order: order ?? this.nextObservationOrder()
|
|
545
|
+
});
|
|
320
546
|
}
|
|
321
547
|
};
|
|
322
548
|
//#endregion
|
|
@@ -331,26 +557,56 @@ var CodexAppServerWire = class {
|
|
|
331
557
|
*/
|
|
332
558
|
/** Default POSIX grace between subprocess termination tiers. */
|
|
333
559
|
const DEFAULT_DISPOSE_GRACE_MS = 3e3;
|
|
560
|
+
const codexPackageJsonPath = createRequire(import.meta.url).resolve("@openai/codex/package.json");
|
|
561
|
+
const codexPackageManifest = JSON.parse(readFileSync(codexPackageJsonPath, "utf8"));
|
|
562
|
+
/** Absolute package-local JavaScript wrapper selected by the package manifest. */
|
|
563
|
+
const CODEX_PACKAGE_BIN = resolve(dirname(codexPackageJsonPath), codexPackageManifest.bin.codex);
|
|
564
|
+
/** Native non-interactive Codex modes mapped to official `thread/start` fields. */
|
|
565
|
+
const CODEX_PERMISSION_MODES = [
|
|
566
|
+
"never",
|
|
567
|
+
"approve-for-me",
|
|
568
|
+
"dangerously-bypass-approvals-and-sandbox"
|
|
569
|
+
];
|
|
570
|
+
/** Safe default for unattended Codex runs. */
|
|
571
|
+
const DEFAULT_CODEX_PERMISSION_MODE = "never";
|
|
572
|
+
function failureDiagnostic(facts) {
|
|
573
|
+
const fields = [
|
|
574
|
+
"product: Codex",
|
|
575
|
+
`stage: ${facts.stage}`,
|
|
576
|
+
`category: ${facts.category}`
|
|
577
|
+
];
|
|
578
|
+
if (facts.httpStatus !== void 0) fields.push(`HTTP status: ${facts.httpStatus}`);
|
|
579
|
+
const processFields = [["exit code", facts.outcome?.exitCode], ["signal", facts.outcome?.signal]];
|
|
580
|
+
for (const [label, value] of processFields) if (value !== null && value !== void 0) fields.push(`${label}: ${value}`);
|
|
581
|
+
return `Product subagent failure (${fields.join("; ")})`;
|
|
582
|
+
}
|
|
583
|
+
var CodexRunFailure = class extends Error {
|
|
584
|
+
facts;
|
|
585
|
+
constructor(facts, cause) {
|
|
586
|
+
super(`subagent-codex: ${failureDiagnostic(facts)}`, cause === void 0 ? void 0 : { cause });
|
|
587
|
+
this.facts = facts;
|
|
588
|
+
this.name = "CodexRunFailure";
|
|
589
|
+
}
|
|
590
|
+
};
|
|
334
591
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* the argv is constant so no task or configuration text enters the
|
|
339
|
-
* shell boundary.
|
|
340
|
-
* @param platform - host platform used to select the executable boundary.
|
|
341
|
-
* @returns argv for the fixed Codex app-server command.
|
|
592
|
+
* Hide an unpublished Host failure behind fixed safe startup facts.
|
|
593
|
+
* @param cause Original Host failure retained for internal diagnostics.
|
|
594
|
+
* @returns A startup failure whose message contains only fixed safe facts.
|
|
342
595
|
*/
|
|
343
|
-
function
|
|
344
|
-
return
|
|
345
|
-
"
|
|
346
|
-
"
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
596
|
+
function codexStartupFailure(cause) {
|
|
597
|
+
return new CodexRunFailure({
|
|
598
|
+
stage: "initialize",
|
|
599
|
+
category: "unknown"
|
|
600
|
+
}, cause);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Fixed package-local app-server command, independent of the host `PATH`.
|
|
604
|
+
* @returns Node, the official wrapper, and the fixed app-server arguments.
|
|
605
|
+
*/
|
|
606
|
+
function codexAppServerArgv() {
|
|
607
|
+
return [
|
|
608
|
+
process.execPath,
|
|
609
|
+
CODEX_PACKAGE_BIN,
|
|
354
610
|
"app-server",
|
|
355
611
|
"--stdio"
|
|
356
612
|
];
|
|
@@ -382,16 +638,30 @@ function textTask(prompt) {
|
|
|
382
638
|
*/
|
|
383
639
|
async function disposeCodexChild(wire, child) {
|
|
384
640
|
wire.close();
|
|
385
|
-
if (child.pid
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
641
|
+
if (child.pid > 0) {
|
|
642
|
+
let outcome;
|
|
643
|
+
child.done.then(
|
|
644
|
+
(value) => {
|
|
645
|
+
outcome = value;
|
|
646
|
+
},
|
|
647
|
+
/* v8 ignore next -- a positive pid excludes spawn-level done rejection. */
|
|
648
|
+
() => {}
|
|
649
|
+
);
|
|
650
|
+
try {
|
|
651
|
+
child.stdin?.end();
|
|
652
|
+
} catch {}
|
|
653
|
+
child.terminate();
|
|
654
|
+
try {
|
|
655
|
+
await child.waitForExit();
|
|
656
|
+
} catch (error) {
|
|
657
|
+
throw new CodexRunFailure({
|
|
658
|
+
stage: "teardown",
|
|
659
|
+
category: "unknown",
|
|
660
|
+
outcome
|
|
661
|
+
}, thrown(error));
|
|
662
|
+
}
|
|
663
|
+
await child.done;
|
|
664
|
+
} else await child.done.catch(() => {});
|
|
395
665
|
}
|
|
396
666
|
/**
|
|
397
667
|
* Start the real `codex app-server --stdio` child and publish its one-shot run.
|
|
@@ -402,20 +672,62 @@ async function disposeCodexChild(wire, child) {
|
|
|
402
672
|
async function startCodexRun(request, spec) {
|
|
403
673
|
const texts = textTask(request.prompt);
|
|
404
674
|
if (request.signal.aborted) throw new Error("subagent-codex: request was aborted before app-server startup");
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
675
|
+
let child;
|
|
676
|
+
try {
|
|
677
|
+
child = spec.spawn({
|
|
678
|
+
argv: codexAppServerArgv(),
|
|
679
|
+
cwd: spec.cwd,
|
|
680
|
+
stdio: {
|
|
681
|
+
stdin: "pipe",
|
|
682
|
+
stdout: "pipe",
|
|
683
|
+
stderr: "pipe"
|
|
684
|
+
},
|
|
685
|
+
graceMs: spec.disposeGraceMs,
|
|
686
|
+
env: spec.env
|
|
687
|
+
});
|
|
688
|
+
} catch (error) {
|
|
689
|
+
throw new CodexRunFailure({
|
|
690
|
+
stage: "initialize",
|
|
691
|
+
category: "unknown"
|
|
692
|
+
}, thrown(error));
|
|
693
|
+
}
|
|
694
|
+
const wire = new CodexAppServerWire(child.stdout, child.stdin, spec.permissionMode);
|
|
695
|
+
const onStderr = (chunk) => {
|
|
696
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
697
|
+
wire.observeStderr(bytes.toString());
|
|
698
|
+
try {
|
|
699
|
+
writeFileSync(process.stderr.fd, bytes);
|
|
700
|
+
} catch {}
|
|
701
|
+
};
|
|
702
|
+
const onStderrError = () => {};
|
|
703
|
+
child.stderr?.on("data", onStderr);
|
|
704
|
+
child.stderr?.on("error", onStderrError);
|
|
705
|
+
const disposeProcess = async () => {
|
|
706
|
+
try {
|
|
707
|
+
await disposeCodexChild(wire, child);
|
|
708
|
+
await new Promise((resolve) => {
|
|
709
|
+
setImmediate(resolve);
|
|
710
|
+
});
|
|
711
|
+
} finally {
|
|
712
|
+
child.stderr?.off("data", onStderr);
|
|
713
|
+
child.stderr?.off("error", onStderrError);
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
let processFailureFacts;
|
|
717
|
+
const processFailure = child.done.then((outcome) => {
|
|
718
|
+
processFailureFacts = {
|
|
719
|
+
stage: "process",
|
|
720
|
+
category: "process-exit",
|
|
721
|
+
outcome
|
|
722
|
+
};
|
|
723
|
+
throw new CodexRunFailure(processFailureFacts);
|
|
724
|
+
}, (error) => {
|
|
725
|
+
processFailureFacts = {
|
|
726
|
+
stage: "process",
|
|
727
|
+
category: "unknown"
|
|
728
|
+
};
|
|
729
|
+
throw new CodexRunFailure(processFailureFacts, thrown(error));
|
|
415
730
|
});
|
|
416
|
-
const wire = new CodexAppServerWire(child.stdout, child.stdin);
|
|
417
|
-
const disposeProcess = () => disposeCodexChild(wire, child);
|
|
418
|
-
const processFailure = child.done.then((outcome) => Promise.reject(/* @__PURE__ */ new Error(`subagent-codex: app-server exited before the run settled (code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`)), (error) => Promise.reject(thrown(error)));
|
|
419
731
|
processFailure.catch(() => {});
|
|
420
732
|
const runAbort = new AbortController();
|
|
421
733
|
const requestCancel = () => {
|
|
@@ -427,24 +739,86 @@ async function startCodexRun(request, spec) {
|
|
|
427
739
|
requestCancel();
|
|
428
740
|
};
|
|
429
741
|
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
742
|
+
let startupStage = "initialize";
|
|
430
743
|
try {
|
|
431
744
|
wire.start();
|
|
432
745
|
await Promise.race([wire.initialize(request.signal), processFailure]);
|
|
746
|
+
startupStage = "thread-start";
|
|
433
747
|
await Promise.race([wire.startThread(spec.cwd, request.signal), processFailure]);
|
|
434
748
|
} catch (error) {
|
|
435
749
|
request.signal.removeEventListener("abort", onAbort);
|
|
750
|
+
const cancelledBeforeCleanup = runAbort.signal.aborted;
|
|
751
|
+
if (!(error instanceof CodexRunFailure) && !cancelledBeforeCleanup) await new Promise((resolve) => {
|
|
752
|
+
setImmediate(resolve);
|
|
753
|
+
});
|
|
754
|
+
const failure = new CodexRunFailure({
|
|
755
|
+
stage: startupStage,
|
|
756
|
+
category: "unknown",
|
|
757
|
+
outcome: error instanceof CodexRunFailure ? error.facts.outcome : processFailureFacts?.outcome
|
|
758
|
+
}, thrown(error));
|
|
436
759
|
try {
|
|
437
760
|
await disposeProcess();
|
|
438
761
|
} catch (disposeError) {
|
|
439
|
-
|
|
762
|
+
const cleanupFailure = thrown(disposeError);
|
|
763
|
+
throw new AggregateError([failure, cleanupFailure], `${failure.message}; ${cleanupFailure.message}`);
|
|
764
|
+
}
|
|
765
|
+
if (cancelledBeforeCleanup) throw new Error("subagent-codex: request was aborted before run publication");
|
|
766
|
+
try {
|
|
767
|
+
request.signal.throwIfAborted();
|
|
768
|
+
} catch {
|
|
769
|
+
throw new Error("subagent-codex: request was aborted before run publication");
|
|
440
770
|
}
|
|
441
|
-
|
|
442
|
-
throw thrown(error);
|
|
771
|
+
throw failure;
|
|
443
772
|
}
|
|
444
773
|
const collectOutput = () => wire.collectOutput();
|
|
774
|
+
let diagnostic;
|
|
775
|
+
const recordFailureDiagnostic = (facts) => {
|
|
776
|
+
const failure = failureDiagnostic(facts);
|
|
777
|
+
const permission = wire.collectDiagnostic();
|
|
778
|
+
diagnostic = permission === void 0 ? failure : `${failure}\n${permission}`;
|
|
779
|
+
return diagnostic;
|
|
780
|
+
};
|
|
781
|
+
const withProcessOutcome = (facts) => {
|
|
782
|
+
const outcome = processFailureFacts?.outcome;
|
|
783
|
+
return outcome === void 0 ? facts : {
|
|
784
|
+
...facts,
|
|
785
|
+
outcome
|
|
786
|
+
};
|
|
787
|
+
};
|
|
788
|
+
const publishedProcessFailure = processFailure.catch(async (error) => {
|
|
789
|
+
await new Promise((resolve) => {
|
|
790
|
+
setImmediate(resolve);
|
|
791
|
+
});
|
|
792
|
+
throw error;
|
|
793
|
+
});
|
|
445
794
|
const result = settleRunResult({
|
|
446
|
-
attempt: () =>
|
|
795
|
+
attempt: async () => {
|
|
796
|
+
try {
|
|
797
|
+
const terminal = await Promise.race([wire.runTurn(texts, runAbort.signal), publishedProcessFailure]);
|
|
798
|
+
if (terminal.stopReason === "completed") return terminal;
|
|
799
|
+
await new Promise((resolve) => {
|
|
800
|
+
setImmediate(resolve);
|
|
801
|
+
});
|
|
802
|
+
const facts = withProcessOutcome(wire.collectFailure());
|
|
803
|
+
return {
|
|
804
|
+
...terminal,
|
|
805
|
+
diagnostic: recordFailureDiagnostic(facts)
|
|
806
|
+
};
|
|
807
|
+
} catch (error) {
|
|
808
|
+
await new Promise((resolve) => {
|
|
809
|
+
setImmediate(resolve);
|
|
810
|
+
});
|
|
811
|
+
const endedBeforeTerminal = wire.endedBeforeTerminal();
|
|
812
|
+
if (endedBeforeTerminal && processFailureFacts === void 0 && !runAbort.signal.aborted) try {
|
|
813
|
+
if (await child.waitForExit(AbortSignal.timeout(Math.ceil(spec.disposeGraceMs)))) await child.done;
|
|
814
|
+
} catch {}
|
|
815
|
+
const facts = error instanceof CodexRunFailure ? error.facts : endedBeforeTerminal && processFailureFacts !== void 0 ? processFailureFacts : withProcessOutcome(wire.collectFailure());
|
|
816
|
+
recordFailureDiagnostic(facts);
|
|
817
|
+
throw error instanceof CodexRunFailure ? error : new CodexRunFailure(facts, thrown(error));
|
|
818
|
+
}
|
|
819
|
+
},
|
|
447
820
|
collectOutput,
|
|
821
|
+
collectDiagnostic: () => diagnostic,
|
|
448
822
|
cancelled: () => runAbort.signal.aborted,
|
|
449
823
|
onError: spec.onError,
|
|
450
824
|
signal: request.signal,
|
|
@@ -462,52 +836,69 @@ async function startCodexRun(request, spec) {
|
|
|
462
836
|
//#endregion
|
|
463
837
|
//#region lib/types/index.js
|
|
464
838
|
/**
|
|
465
|
-
*
|
|
466
|
-
* official `
|
|
467
|
-
* workspace and publishes only after an ephemeral thread exists.
|
|
839
|
+
* Profile-named Codex one-shot subagent provider. Every accepted run starts a
|
|
840
|
+
* fresh official package-local Codex wrapper with `app-server --stdio` in the
|
|
841
|
+
* delegating Session's workspace and publishes only after an ephemeral thread exists.
|
|
468
842
|
*
|
|
469
843
|
* @module @deepseek-ai/dsh-subagent-codex
|
|
470
844
|
*/
|
|
471
845
|
const name = "subagent-codex";
|
|
472
846
|
const inject = ["subagents", "subprocess"];
|
|
847
|
+
const DEFAULT_PROVIDER_NAME = "codex";
|
|
473
848
|
const Config = z.object({
|
|
849
|
+
providerName: z.string().min(1).default(DEFAULT_PROVIDER_NAME),
|
|
474
850
|
env: z.dict(z.string()).default({}),
|
|
851
|
+
permissionMode: z.union([...CODEX_PERMISSION_MODES]).default(DEFAULT_CODEX_PERMISSION_MODE),
|
|
475
852
|
disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS)
|
|
476
853
|
});
|
|
477
854
|
var CodexProvider = class {
|
|
855
|
+
name;
|
|
478
856
|
ctx;
|
|
479
857
|
config;
|
|
480
|
-
name = "codex";
|
|
481
858
|
capabilities = NO_START_CAPABILITIES;
|
|
482
859
|
inheritsParentContext = false;
|
|
483
|
-
constructor(ctx, config) {
|
|
860
|
+
constructor(name, ctx, config) {
|
|
861
|
+
this.name = name;
|
|
484
862
|
this.ctx = ctx;
|
|
485
863
|
this.config = config;
|
|
486
864
|
}
|
|
487
865
|
start(request) {
|
|
488
866
|
const parentCwd = request.parent.session.header.cwd;
|
|
489
867
|
if (parentCwd === void 0) throw new Error("subagent-codex: no working directory for the child — delegate from a parent session that has one");
|
|
868
|
+
let cwd;
|
|
869
|
+
try {
|
|
870
|
+
cwd = resolveChildCwd("subagent-codex", void 0, parentCwd);
|
|
871
|
+
} catch (error) {
|
|
872
|
+
if (request.signal.aborted) throw new Error("subagent-codex: request was aborted before app-server startup");
|
|
873
|
+
throw codexStartupFailure(error);
|
|
874
|
+
}
|
|
490
875
|
return startCodexRun(request, {
|
|
491
|
-
cwd
|
|
876
|
+
cwd,
|
|
877
|
+
permissionMode: this.config.permissionMode,
|
|
492
878
|
env: this.config.env,
|
|
493
879
|
disposeGraceMs: this.config.disposeGraceMs,
|
|
494
880
|
spawn: (spawnSpec) => this.ctx.subprocess.spawn(spawnSpec),
|
|
495
881
|
onError: (error, stopReason) => {
|
|
496
|
-
this.ctx.logger.warn(`subagent-codex: child run failed (${stopReason}): ${error.message}`);
|
|
882
|
+
this.ctx.logger.warn(`subagent-codex "${this.name}": child run failed (${stopReason}): ${error.message}`);
|
|
497
883
|
}
|
|
498
884
|
});
|
|
499
885
|
}
|
|
500
886
|
};
|
|
501
887
|
/**
|
|
502
|
-
* Register
|
|
888
|
+
* Register one Profile-named Codex provider.
|
|
503
889
|
* @param ctx - context carrying shared subagent and subprocess services.
|
|
504
|
-
* @param config -
|
|
890
|
+
* @param config - registry name, permission mode, child environment, and disposal grace.
|
|
505
891
|
*/
|
|
506
892
|
function apply(ctx, config) {
|
|
507
|
-
const resolved =
|
|
893
|
+
const resolved = {
|
|
894
|
+
providerName: config.providerName ?? DEFAULT_PROVIDER_NAME,
|
|
895
|
+
env: config.env,
|
|
896
|
+
permissionMode: config.permissionMode ?? "never",
|
|
897
|
+
disposeGraceMs: config.disposeGraceMs
|
|
898
|
+
};
|
|
508
899
|
assertPositiveFinite("subagent-codex", "disposeGraceMs", resolved.disposeGraceMs);
|
|
509
900
|
if (resolved.disposeGraceMs > MAX_TIMER_DELAY_MS) throw new Error(`subagent-codex: disposeGraceMs must be no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
510
|
-
ctx.subagents.registerProvider(new CodexProvider(ctx, resolved));
|
|
901
|
+
ctx.subagents.registerProvider(new CodexProvider(resolved.providerName, ctx, resolved));
|
|
511
902
|
}
|
|
512
903
|
//#endregion
|
|
513
904
|
export { Config, apply, inject, name };
|