@triflux/remote 10.51.0 → 10.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cto/brief.mjs +0 -8
- package/cto/collect.mjs +1 -127
- package/cto/current.schema.json +2 -19
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -139
- package/hub/index.mjs +0 -9
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/cli/commands/debug.mjs +1 -2
- package/hub/team/cli/commands/start/index.mjs +2 -1
- package/hub/team/cli/commands/start/start-mux.mjs +2 -4
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/headless.mjs +96 -543
- package/hub/team/psmux.mjs +3 -33
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/tui-viewer.mjs +9 -56
- package/hub/team/tui.mjs +0 -37
- package/hub/team/uds-orchestrator.mjs +2 -580
- package/hub/workers/factory.mjs +6 -51
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +0 -67
- package/scripts/lib/cli-claude.mjs +0 -12
- package/scripts/lib/env-probe.mjs +0 -70
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/cli/services/hub-client.mjs +0 -178
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-intent.mjs +0 -18
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/tools.mjs +0 -900
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -26
- package/scripts/lib/context.mjs +0 -51
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -1,1067 +0,0 @@
|
|
|
1
|
-
// hub/workers/codex-app-server-worker.mjs — Codex app-server JSON-RPC 2.0 worker
|
|
2
|
-
//
|
|
3
|
-
// Spawns `codex app-server --listen stdio://` and streams notifications as
|
|
4
|
-
// `agent.progress` publish events. Shares the `WorkerInterface` shape with
|
|
5
|
-
// `codex-mcp.mjs` for factory compatibility but uses a different transport.
|
|
6
|
-
//
|
|
7
|
-
// Phase 3 / PRD-2 — implements AC1..AC17 (except AC9 factory integration which
|
|
8
|
-
// lands in PRD-3 and AC11 regression which is purely "do not touch codex-mcp").
|
|
9
|
-
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
-
import { isAbsolute } from "node:path";
|
|
13
|
-
import process from "node:process";
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
createActivityLifecycle,
|
|
17
|
-
isActivityLifecycleEnabled,
|
|
18
|
-
resolveHardCeilingMs,
|
|
19
|
-
resolveStallInterventionMs,
|
|
20
|
-
} from "@triflux/core/hub/lib/worker-lifecycle.mjs";
|
|
21
|
-
import {
|
|
22
|
-
JsonRpcProtocolError,
|
|
23
|
-
JsonRpcStdioClient,
|
|
24
|
-
JsonRpcTransportError,
|
|
25
|
-
} from "./lib/jsonrpc-stdio.mjs";
|
|
26
|
-
|
|
27
|
-
// ── Exit codes ──────────────────────────────────────────────────
|
|
28
|
-
// Reuse codex-mcp convention so factory + retry logic can stay uniform.
|
|
29
|
-
export const CODEX_APP_SERVER_EXECUTION_EXIT_CODE = 1;
|
|
30
|
-
export const CODEX_APP_SERVER_TRANSPORT_EXIT_CODE = 70;
|
|
31
|
-
export const CODEX_APP_SERVER_TIMEOUT_EXIT_CODE = 124;
|
|
32
|
-
|
|
33
|
-
export const DEFAULT_CODEX_APP_SERVER_BOOTSTRAP_TIMEOUT_MS = 10_000;
|
|
34
|
-
// Legacy wall-clock default retained for TFX_ACTIVITY_LIFECYCLE=0 compatibility.
|
|
35
|
-
export const DEFAULT_CODEX_APP_SERVER_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000;
|
|
36
|
-
export const DEFAULT_UNKNOWN_METHOD_WARN_THRESHOLD = 5;
|
|
37
|
-
/**
|
|
38
|
-
* How long stop() waits for `thread/unsubscribe` response before forcing
|
|
39
|
-
* SIGTERM. Keep small — unsubscribe is a best-effort unload signal.
|
|
40
|
-
*/
|
|
41
|
-
export const UNSUBSCRIBE_DEADLINE_MS = 2_000;
|
|
42
|
-
|
|
43
|
-
// ── Error classes ───────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Transport/bootstrap failures (spawn ENOENT, initialize timeout, stream close).
|
|
47
|
-
*/
|
|
48
|
-
export class CodexAppServerTransportError extends Error {
|
|
49
|
-
/**
|
|
50
|
-
* @param {string} message
|
|
51
|
-
* @param {{ cause?: unknown, stderr?: string }} [options]
|
|
52
|
-
*/
|
|
53
|
-
constructor(message, options = {}) {
|
|
54
|
-
super(message, { cause: options.cause });
|
|
55
|
-
this.name = "CodexAppServerTransportError";
|
|
56
|
-
this.stderr = options.stderr || "";
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Protocol-level failures (malformed JSON, max line exceeded, unresolved pending
|
|
62
|
-
* requests on EOF).
|
|
63
|
-
*/
|
|
64
|
-
export class CodexAppServerProtocolError extends Error {
|
|
65
|
-
/** @param {string} message @param {{ cause?: unknown }} [options] */
|
|
66
|
-
constructor(message, options = {}) {
|
|
67
|
-
super(message, { cause: options.cause });
|
|
68
|
-
this.name = "CodexAppServerProtocolError";
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Protocol constants ──────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
/** 24 notification methods we forward as publish events. */
|
|
75
|
-
export const SUBSCRIBED_METHODS = Object.freeze(
|
|
76
|
-
new Set([
|
|
77
|
-
// thread lifecycle
|
|
78
|
-
"thread/started",
|
|
79
|
-
"thread/closed",
|
|
80
|
-
"thread/tokenUsage/updated",
|
|
81
|
-
"thread/compacted",
|
|
82
|
-
// turn lifecycle
|
|
83
|
-
"turn/started",
|
|
84
|
-
"turn/completed",
|
|
85
|
-
"turn/diff/updated",
|
|
86
|
-
"turn/plan/updated",
|
|
87
|
-
// item lifecycle
|
|
88
|
-
"item/started",
|
|
89
|
-
"item/completed",
|
|
90
|
-
"item/autoApprovalReview/started",
|
|
91
|
-
"item/autoApprovalReview/completed",
|
|
92
|
-
// streaming deltas
|
|
93
|
-
"item/agentMessage/delta",
|
|
94
|
-
"item/reasoning/summaryTextDelta",
|
|
95
|
-
"item/reasoning/textDelta",
|
|
96
|
-
"item/reasoning/summaryPartAdded",
|
|
97
|
-
"item/plan/delta",
|
|
98
|
-
"item/mcpToolCall/progress",
|
|
99
|
-
"command/exec/outputDelta",
|
|
100
|
-
"item/commandExecution/outputDelta",
|
|
101
|
-
"item/commandExecution/terminalInteraction",
|
|
102
|
-
"item/fileChange/outputDelta",
|
|
103
|
-
// errors / model
|
|
104
|
-
"error",
|
|
105
|
-
"model/rerouted",
|
|
106
|
-
"configWarning",
|
|
107
|
-
]),
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
/** 27 notification methods registered at initialize so codex never emits them. */
|
|
111
|
-
export const OPT_OUT_METHODS = Object.freeze([
|
|
112
|
-
"skills/changed",
|
|
113
|
-
"mcpServer/oauthLogin/completed",
|
|
114
|
-
"mcpServer/startupStatus/updated",
|
|
115
|
-
"account/updated",
|
|
116
|
-
"account/rateLimits/updated",
|
|
117
|
-
"account/login/completed",
|
|
118
|
-
"app/list/updated",
|
|
119
|
-
"fs/changed",
|
|
120
|
-
"thread/realtime/started",
|
|
121
|
-
"thread/realtime/itemAdded",
|
|
122
|
-
"thread/realtime/transcriptUpdated",
|
|
123
|
-
"thread/realtime/outputAudio/delta",
|
|
124
|
-
"thread/realtime/sdp",
|
|
125
|
-
"thread/realtime/error",
|
|
126
|
-
"thread/realtime/closed",
|
|
127
|
-
"fuzzyFileSearch/sessionUpdated",
|
|
128
|
-
"fuzzyFileSearch/sessionCompleted",
|
|
129
|
-
"windows/worldWritableWarning",
|
|
130
|
-
"windowsSandbox/setupCompleted",
|
|
131
|
-
"rawResponseItem/completed",
|
|
132
|
-
"serverRequest/resolved",
|
|
133
|
-
"hook/started",
|
|
134
|
-
"hook/completed",
|
|
135
|
-
"thread/archived",
|
|
136
|
-
"thread/unarchived",
|
|
137
|
-
"thread/name/updated",
|
|
138
|
-
"deprecationNotice",
|
|
139
|
-
]);
|
|
140
|
-
|
|
141
|
-
/** Method → publish `kind` (13 distinct kinds). */
|
|
142
|
-
export const KIND_MAP = Object.freeze({
|
|
143
|
-
"thread/started": "thread_status",
|
|
144
|
-
"thread/closed": "thread_status",
|
|
145
|
-
"thread/tokenUsage/updated": "thread_status",
|
|
146
|
-
"thread/compacted": "thread_status",
|
|
147
|
-
"turn/started": "turn_status",
|
|
148
|
-
"turn/completed": "turn_status",
|
|
149
|
-
"turn/diff/updated": "turn_status",
|
|
150
|
-
"turn/plan/updated": "turn_status",
|
|
151
|
-
"item/started": "item_status",
|
|
152
|
-
"item/completed": "item_status",
|
|
153
|
-
"item/autoApprovalReview/started": "approval_review",
|
|
154
|
-
"item/autoApprovalReview/completed": "approval_review",
|
|
155
|
-
"item/agentMessage/delta": "text_delta",
|
|
156
|
-
"item/reasoning/summaryTextDelta": "thinking_delta",
|
|
157
|
-
"item/reasoning/textDelta": "thinking_delta",
|
|
158
|
-
"item/reasoning/summaryPartAdded": "thinking_delta",
|
|
159
|
-
"item/plan/delta": "plan_delta",
|
|
160
|
-
"item/mcpToolCall/progress": "tool_progress",
|
|
161
|
-
"command/exec/outputDelta": "exec_delta",
|
|
162
|
-
"item/commandExecution/outputDelta": "exec_delta",
|
|
163
|
-
"item/commandExecution/terminalInteraction": "terminal_interaction",
|
|
164
|
-
"item/fileChange/outputDelta": "file_delta",
|
|
165
|
-
error: "error",
|
|
166
|
-
"model/rerouted": "error",
|
|
167
|
-
configWarning: "error",
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// NOTE: codex 0.135.0 rejects `--skip-git-repo-check` as a global option (exit 2:
|
|
171
|
-
// "unexpected argument"); it lives at `codex exec` subcommand scope, and the
|
|
172
|
-
// `app-server` subcommand does not need it. Empirically verified against
|
|
173
|
-
// codex-cli 0.135.0. Do not re-add it as a global flag here.
|
|
174
|
-
export const DEFAULT_CODEX_APP_SERVER_ARGS = Object.freeze([
|
|
175
|
-
"app-server",
|
|
176
|
-
"--listen",
|
|
177
|
-
"stdio://",
|
|
178
|
-
]);
|
|
179
|
-
|
|
180
|
-
// ── Helpers ─────────────────────────────────────────────────────
|
|
181
|
-
|
|
182
|
-
function cloneEnv(env = process.env) {
|
|
183
|
-
return Object.fromEntries(
|
|
184
|
-
Object.entries(env).filter(([, value]) => typeof value === "string"),
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Strictly monotonic `Date.now()` — if two publishes collide in the same tick we
|
|
190
|
-
* bump by 1 ms. Keeps AC12 simple without `performance.now` float math.
|
|
191
|
-
*/
|
|
192
|
-
function createMonotonicClock() {
|
|
193
|
-
let last = 0;
|
|
194
|
-
return () => {
|
|
195
|
-
const now = Date.now();
|
|
196
|
-
last = now > last ? now : last + 1;
|
|
197
|
-
return last;
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Walk a publish `data` payload and hash any string longer than 32 chars (AC17).
|
|
203
|
-
* Short identifiers (ids, statuses) are kept readable.
|
|
204
|
-
*/
|
|
205
|
-
function redactDeep(value) {
|
|
206
|
-
if (value == null) return value;
|
|
207
|
-
if (typeof value === "string") {
|
|
208
|
-
if (value.length <= 32) return value;
|
|
209
|
-
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
210
|
-
}
|
|
211
|
-
if (Array.isArray(value)) return value.map(redactDeep);
|
|
212
|
-
if (typeof value === "object") {
|
|
213
|
-
const out = {};
|
|
214
|
-
for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v);
|
|
215
|
-
return out;
|
|
216
|
-
}
|
|
217
|
-
return value;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function buildSpawnOptions({ cwd, env, command }) {
|
|
221
|
-
return {
|
|
222
|
-
cwd,
|
|
223
|
-
env,
|
|
224
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
225
|
-
// Windows needs shell for PATHEXT/ENOENT resolution when command is not absolute
|
|
226
|
-
shell: process.platform === "win32" && !isAbsolute(command),
|
|
227
|
-
windowsHide: true,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function buildInitializeParams(clientInfo) {
|
|
232
|
-
return {
|
|
233
|
-
clientInfo,
|
|
234
|
-
capabilities: {
|
|
235
|
-
experimentalApi: true,
|
|
236
|
-
optOutNotificationMethods: [...OPT_OUT_METHODS],
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function buildThreadStartParams(opts = {}) {
|
|
242
|
-
const params = {
|
|
243
|
-
sandbox: opts.sandbox || "read-only",
|
|
244
|
-
approvalPolicy: opts.approvalPolicy || "never",
|
|
245
|
-
ephemeral: true,
|
|
246
|
-
experimentalRawEvents: false,
|
|
247
|
-
persistExtendedHistory: false,
|
|
248
|
-
};
|
|
249
|
-
if (typeof opts.model === "string" && opts.model) params.model = opts.model;
|
|
250
|
-
if (typeof opts.profile === "string" && opts.profile) {
|
|
251
|
-
params.serviceName = opts.profile;
|
|
252
|
-
}
|
|
253
|
-
if (opts.config && typeof opts.config === "object")
|
|
254
|
-
params.config = opts.config;
|
|
255
|
-
if (typeof opts.cwd === "string" && opts.cwd) params.cwd = opts.cwd;
|
|
256
|
-
if (typeof opts.baseInstructions === "string" && opts.baseInstructions) {
|
|
257
|
-
params.baseInstructions = opts.baseInstructions;
|
|
258
|
-
}
|
|
259
|
-
if (
|
|
260
|
-
typeof opts.developerInstructions === "string" &&
|
|
261
|
-
opts.developerInstructions
|
|
262
|
-
) {
|
|
263
|
-
params.developerInstructions = opts.developerInstructions;
|
|
264
|
-
}
|
|
265
|
-
return params;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function buildTurnStartParams(threadId, prompt) {
|
|
269
|
-
return {
|
|
270
|
-
threadId,
|
|
271
|
-
input: [{ type: "text", text: prompt, text_elements: [] }],
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function buildWorkerError(code, message, attempts = 1) {
|
|
276
|
-
return Object.freeze({
|
|
277
|
-
code,
|
|
278
|
-
retryable: code === "CODEX_APP_SERVER_TRANSPORT_ERROR",
|
|
279
|
-
attempts,
|
|
280
|
-
category:
|
|
281
|
-
code === "INVALID_INPUT"
|
|
282
|
-
? "input"
|
|
283
|
-
: code === "CODEX_APP_SERVER_TRANSPORT_ERROR"
|
|
284
|
-
? "transient"
|
|
285
|
-
: "config",
|
|
286
|
-
recovery:
|
|
287
|
-
code === "INVALID_INPUT"
|
|
288
|
-
? "Provide a non-empty prompt before invoking the Codex app-server worker."
|
|
289
|
-
: "Review the codex app-server stderr tail and retry.",
|
|
290
|
-
message,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function extractThreadIdFromStartResponse(result) {
|
|
295
|
-
if (!result || typeof result !== "object") return null;
|
|
296
|
-
if (typeof result.threadId === "string") return result.threadId;
|
|
297
|
-
if (result.thread && typeof result.thread.id === "string") {
|
|
298
|
-
return result.thread.id;
|
|
299
|
-
}
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function extractThreadIdFromStartedNotif(params) {
|
|
304
|
-
if (!params || typeof params !== "object") return null;
|
|
305
|
-
if (typeof params.threadId === "string") return params.threadId;
|
|
306
|
-
if (params.thread && typeof params.thread.id === "string") {
|
|
307
|
-
return params.thread.id;
|
|
308
|
-
}
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ── Worker class ────────────────────────────────────────────────
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Codex app-server worker.
|
|
316
|
-
* @implements {import('./interface.mjs').IWorker}
|
|
317
|
-
*/
|
|
318
|
-
export class CodexAppServerWorker {
|
|
319
|
-
type = "codex";
|
|
320
|
-
transport = "app-server";
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* @param {object} [options]
|
|
324
|
-
* @param {string} [options.command]
|
|
325
|
-
* @param {string[]} [options.args]
|
|
326
|
-
* @param {string} [options.cwd]
|
|
327
|
-
* @param {Record<string, string>} [options.env]
|
|
328
|
-
* @param {{ name: string, version: string }} [options.clientInfo]
|
|
329
|
-
* @param {number} [options.bootstrapTimeoutMs]
|
|
330
|
-
* @param {(msg: object) => void | Promise<void>} [options.publishCallback]
|
|
331
|
-
* @param {string} [options.workerId]
|
|
332
|
-
* @param {(command: string, args: string[], options: object) => import('node:child_process').ChildProcess} [options.spawnFn]
|
|
333
|
-
* @param {boolean} [options.redactSensitive]
|
|
334
|
-
* @param {number} [options.unknownMethodWarnThreshold]
|
|
335
|
-
* @param {(method: string) => void} [options.onUnknownMethod]
|
|
336
|
-
* @param {(label: string, payload: object) => void} [options.warn]
|
|
337
|
-
* @param {(context: object) => Promise<boolean|string>|boolean|string} [options.interveneFn]
|
|
338
|
-
*/
|
|
339
|
-
constructor(options = {}) {
|
|
340
|
-
this.command = options.command || process.env.CODEX_BIN || "codex";
|
|
341
|
-
this.args =
|
|
342
|
-
Array.isArray(options.args) && options.args.length
|
|
343
|
-
? [...options.args]
|
|
344
|
-
: [...DEFAULT_CODEX_APP_SERVER_ARGS];
|
|
345
|
-
this.cwd = options.cwd || process.cwd();
|
|
346
|
-
this.env = cloneEnv({ ...cloneEnv(process.env), ...cloneEnv(options.env) });
|
|
347
|
-
this.clientInfo = options.clientInfo || {
|
|
348
|
-
name: "triflux-codex-app-server",
|
|
349
|
-
version: "1.0.0",
|
|
350
|
-
};
|
|
351
|
-
this.bootstrapTimeoutMs = Number.isFinite(options.bootstrapTimeoutMs)
|
|
352
|
-
? options.bootstrapTimeoutMs
|
|
353
|
-
: DEFAULT_CODEX_APP_SERVER_BOOTSTRAP_TIMEOUT_MS;
|
|
354
|
-
this.publishCallback =
|
|
355
|
-
typeof options.publishCallback === "function"
|
|
356
|
-
? options.publishCallback
|
|
357
|
-
: null;
|
|
358
|
-
this.workerId = options.workerId || `codex-app-server-${randomUUID()}`;
|
|
359
|
-
this._spawnFn = options.spawnFn || spawn;
|
|
360
|
-
this._JsonRpcClientClass =
|
|
361
|
-
typeof options.jsonRpcClientClass === "function"
|
|
362
|
-
? options.jsonRpcClientClass
|
|
363
|
-
: JsonRpcStdioClient;
|
|
364
|
-
this.redactSensitive =
|
|
365
|
-
options.redactSensitive === true || process.env.TFX_CODEX_REDACT === "1";
|
|
366
|
-
this.unknownMethodWarnThreshold = Number.isFinite(
|
|
367
|
-
options.unknownMethodWarnThreshold,
|
|
368
|
-
)
|
|
369
|
-
? options.unknownMethodWarnThreshold
|
|
370
|
-
: DEFAULT_UNKNOWN_METHOD_WARN_THRESHOLD;
|
|
371
|
-
this._warn =
|
|
372
|
-
typeof options.warn === "function"
|
|
373
|
-
? options.warn
|
|
374
|
-
: (label, payload) => {
|
|
375
|
-
console.warn(`[codex-app-server] ${label}`, payload);
|
|
376
|
-
};
|
|
377
|
-
this._onUnknownMethod =
|
|
378
|
-
typeof options.onUnknownMethod === "function"
|
|
379
|
-
? options.onUnknownMethod
|
|
380
|
-
: null;
|
|
381
|
-
this._interveneFn =
|
|
382
|
-
typeof options.interveneFn === "function" ? options.interveneFn : null;
|
|
383
|
-
|
|
384
|
-
/** @type {import('node:child_process').ChildProcess | null} */
|
|
385
|
-
this.child = null;
|
|
386
|
-
/** @type {JsonRpcStdioClient | null} */
|
|
387
|
-
this.client = null;
|
|
388
|
-
this.ready = false;
|
|
389
|
-
/** Ring buffer of the last 16 KB of stderr for diagnostics. */
|
|
390
|
-
this.serverStderr = "";
|
|
391
|
-
/** @type {string | null} */
|
|
392
|
-
this.activeThreadId = null;
|
|
393
|
-
this._warnedUnknown = new Set();
|
|
394
|
-
this._monotonicNow = createMonotonicClock();
|
|
395
|
-
this._publishQueue = [];
|
|
396
|
-
this._publishMaxQueue = 64;
|
|
397
|
-
/** @type {Function | null} */
|
|
398
|
-
this._rejectBootstrap = null;
|
|
399
|
-
/**
|
|
400
|
-
* Issue #95 P1 #2 — in-flight execute fail-fast registry.
|
|
401
|
-
* Each execute() registers `(err) => void` here; child `exit`/`error`
|
|
402
|
-
* and JsonRpcTransport/ProtocolError hook drain this to reject immediately
|
|
403
|
-
* instead of waiting for timeoutMs.
|
|
404
|
-
* @type {Set<(err: Error) => void>}
|
|
405
|
-
*/
|
|
406
|
-
this._inflightRejectors = new Set();
|
|
407
|
-
/** Unsubscribe for child exit/error listeners wired in start(). */
|
|
408
|
-
this._detachChildLifecycle = null;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
isReady() {
|
|
412
|
-
return this.ready === true;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Spawn the codex app-server child, wire JsonRpcStdioClient, and complete the
|
|
417
|
-
* initialize/initialized handshake.
|
|
418
|
-
* @returns {Promise<void>}
|
|
419
|
-
*/
|
|
420
|
-
async start() {
|
|
421
|
-
if (this.ready) return;
|
|
422
|
-
await this.stop().catch(() => {});
|
|
423
|
-
|
|
424
|
-
let child;
|
|
425
|
-
try {
|
|
426
|
-
child = this._spawnFn(
|
|
427
|
-
this.command,
|
|
428
|
-
this.args,
|
|
429
|
-
buildSpawnOptions({
|
|
430
|
-
cwd: this.cwd,
|
|
431
|
-
env: this.env,
|
|
432
|
-
command: this.command,
|
|
433
|
-
}),
|
|
434
|
-
);
|
|
435
|
-
} catch (error) {
|
|
436
|
-
throw new CodexAppServerTransportError(
|
|
437
|
-
`codex app-server spawn 실패: ${
|
|
438
|
-
error instanceof Error ? error.message : String(error)
|
|
439
|
-
}`,
|
|
440
|
-
{ cause: error },
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (!child?.stdin || !child.stdout) {
|
|
445
|
-
throw new CodexAppServerTransportError(
|
|
446
|
-
"codex app-server 자식 프로세스의 stdio 핸들을 가져오지 못했습니다.",
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
this.child = child;
|
|
451
|
-
this.serverStderr = "";
|
|
452
|
-
if (child.stderr && typeof child.stderr.on === "function") {
|
|
453
|
-
child.stderr.on("data", (chunk) => {
|
|
454
|
-
this.serverStderr += String(chunk);
|
|
455
|
-
if (this.serverStderr.length > 16_000) {
|
|
456
|
-
this.serverStderr = this.serverStderr.slice(-16_000);
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const spawnErrorPromise = new Promise((_, reject) => {
|
|
462
|
-
const onError = (err) => {
|
|
463
|
-
reject(
|
|
464
|
-
new CodexAppServerTransportError(
|
|
465
|
-
`codex app-server 프로세스 오류: ${err?.message || err}`,
|
|
466
|
-
{ cause: err, stderr: this.serverStderr },
|
|
467
|
-
),
|
|
468
|
-
);
|
|
469
|
-
};
|
|
470
|
-
const onExit = (code, signal) => {
|
|
471
|
-
if (this.ready) return;
|
|
472
|
-
reject(
|
|
473
|
-
new CodexAppServerTransportError(
|
|
474
|
-
`codex app-server 프로세스가 조기 종료됨 (code=${code}, signal=${signal || ""})`,
|
|
475
|
-
{ stderr: this.serverStderr },
|
|
476
|
-
),
|
|
477
|
-
);
|
|
478
|
-
};
|
|
479
|
-
child.once("error", onError);
|
|
480
|
-
child.once("exit", onExit);
|
|
481
|
-
this._rejectBootstrap = (err) => {
|
|
482
|
-
child.removeListener("error", onError);
|
|
483
|
-
child.removeListener("exit", onExit);
|
|
484
|
-
reject(err);
|
|
485
|
-
};
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
const JsonRpcClientCtor = this._JsonRpcClientClass;
|
|
489
|
-
this.client = new JsonRpcClientCtor({
|
|
490
|
-
stdin: child.stdout,
|
|
491
|
-
stdout: child.stdin,
|
|
492
|
-
onError: (err) => {
|
|
493
|
-
this._warn("jsonrpc error", { message: err?.message || String(err) });
|
|
494
|
-
// P1 #2 / P1 #3 fail-fast: structural protocol/transport errors during
|
|
495
|
-
// an active turn must reject in-flight execute() immediately instead of
|
|
496
|
-
// silently hanging until timeoutMs. Parse noise without in-flight work
|
|
497
|
-
// stays warn-only via _warn above.
|
|
498
|
-
if (
|
|
499
|
-
(err instanceof JsonRpcProtocolError ||
|
|
500
|
-
err instanceof JsonRpcTransportError) &&
|
|
501
|
-
this._inflightRejectors.size > 0
|
|
502
|
-
) {
|
|
503
|
-
const transportErr = new CodexAppServerTransportError(
|
|
504
|
-
`codex app-server transport error: ${err.message}`,
|
|
505
|
-
{ cause: err, stderr: this.serverStderr },
|
|
506
|
-
);
|
|
507
|
-
this._drainInflight(transportErr);
|
|
508
|
-
}
|
|
509
|
-
},
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// Initialize handshake with timeout.
|
|
513
|
-
const initPromise = this.client.request(
|
|
514
|
-
"initialize",
|
|
515
|
-
buildInitializeParams(this.clientInfo),
|
|
516
|
-
this.bootstrapTimeoutMs,
|
|
517
|
-
);
|
|
518
|
-
|
|
519
|
-
try {
|
|
520
|
-
await Promise.race([initPromise, spawnErrorPromise]);
|
|
521
|
-
} catch (error) {
|
|
522
|
-
await this.stop().catch(() => {});
|
|
523
|
-
if (error instanceof CodexAppServerTransportError) throw error;
|
|
524
|
-
throw new CodexAppServerTransportError(
|
|
525
|
-
`codex app-server initialize 실패: ${
|
|
526
|
-
error instanceof Error ? error.message : String(error)
|
|
527
|
-
}`,
|
|
528
|
-
{ cause: error, stderr: this.serverStderr },
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
this._rejectBootstrap = null;
|
|
533
|
-
try {
|
|
534
|
-
this.client.notify("initialized", {});
|
|
535
|
-
} catch (error) {
|
|
536
|
-
await this.stop().catch(() => {});
|
|
537
|
-
throw new CodexAppServerTransportError(
|
|
538
|
-
`codex app-server initialized notification 실패: ${
|
|
539
|
-
error instanceof Error ? error.message : String(error)
|
|
540
|
-
}`,
|
|
541
|
-
{ cause: error, stderr: this.serverStderr },
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// P1 #2: post-bootstrap lifecycle listeners. Once the handshake succeeds
|
|
546
|
-
// (`ready=true`) the spawn-time `once("exit"/"error")` handlers are
|
|
547
|
-
// consumed by `_rejectBootstrap` — we now install long-lived listeners so
|
|
548
|
-
// that a mid-turn child crash rejects in-flight execute() immediately.
|
|
549
|
-
const onPostBootstrapError = (err) => {
|
|
550
|
-
const transportErr = new CodexAppServerTransportError(
|
|
551
|
-
`codex app-server 프로세스 오류 (mid-turn): ${err?.message || err}`,
|
|
552
|
-
{ cause: err, stderr: this.serverStderr },
|
|
553
|
-
);
|
|
554
|
-
this._drainInflight(transportErr);
|
|
555
|
-
};
|
|
556
|
-
const onPostBootstrapExit = (code, signal) => {
|
|
557
|
-
this.ready = false;
|
|
558
|
-
if (this._inflightRejectors.size === 0) return;
|
|
559
|
-
const transportErr = new CodexAppServerTransportError(
|
|
560
|
-
`codex app-server 프로세스가 턴 중 종료됨 (code=${code}, signal=${signal || ""})`,
|
|
561
|
-
{ stderr: this.serverStderr },
|
|
562
|
-
);
|
|
563
|
-
this._drainInflight(transportErr);
|
|
564
|
-
};
|
|
565
|
-
child.on("error", onPostBootstrapError);
|
|
566
|
-
child.on("exit", onPostBootstrapExit);
|
|
567
|
-
this._detachChildLifecycle = () => {
|
|
568
|
-
try {
|
|
569
|
-
child.removeListener("error", onPostBootstrapError);
|
|
570
|
-
} catch {}
|
|
571
|
-
try {
|
|
572
|
-
child.removeListener("exit", onPostBootstrapExit);
|
|
573
|
-
} catch {}
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
this.ready = true;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Fail-fast hook: reject every registered in-flight execute() with `err`.
|
|
581
|
-
* Called on child exit/error, transport/protocol error, or stop().
|
|
582
|
-
* @param {Error} err
|
|
583
|
-
*/
|
|
584
|
-
_drainInflight(err) {
|
|
585
|
-
if (this._inflightRejectors.size === 0) return;
|
|
586
|
-
const rejectors = [...this._inflightRejectors];
|
|
587
|
-
this._inflightRejectors.clear();
|
|
588
|
-
for (const reject of rejectors) {
|
|
589
|
-
try {
|
|
590
|
-
reject(err);
|
|
591
|
-
} catch {
|
|
592
|
-
/* never throw out of fail-fast path */
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Run a single prompt through `thread/start` + `turn/start` and resolve when
|
|
599
|
-
* the turn completes (or errors out).
|
|
600
|
-
* @param {string} prompt
|
|
601
|
-
* @param {import('./interface.mjs').WorkerExecuteOptions} [opts]
|
|
602
|
-
* @returns {Promise<import('./interface.mjs').WorkerResult>}
|
|
603
|
-
*/
|
|
604
|
-
async execute(prompt, opts = {}) {
|
|
605
|
-
const sessionKey =
|
|
606
|
-
typeof opts.sessionKey === "string" && opts.sessionKey
|
|
607
|
-
? opts.sessionKey
|
|
608
|
-
: null;
|
|
609
|
-
|
|
610
|
-
if (typeof prompt !== "string" || !prompt.trim()) {
|
|
611
|
-
return {
|
|
612
|
-
output: "prompt는 비어 있을 수 없습니다.",
|
|
613
|
-
exitCode: CODEX_APP_SERVER_EXECUTION_EXIT_CODE,
|
|
614
|
-
threadId: null,
|
|
615
|
-
sessionKey,
|
|
616
|
-
error: buildWorkerError(
|
|
617
|
-
"INVALID_INPUT",
|
|
618
|
-
"prompt는 비어 있을 수 없습니다.",
|
|
619
|
-
),
|
|
620
|
-
raw: null,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
try {
|
|
625
|
-
await this.start();
|
|
626
|
-
} catch (error) {
|
|
627
|
-
return {
|
|
628
|
-
output: error instanceof Error ? error.message : String(error),
|
|
629
|
-
exitCode: CODEX_APP_SERVER_TRANSPORT_EXIT_CODE,
|
|
630
|
-
threadId: null,
|
|
631
|
-
sessionKey,
|
|
632
|
-
error: buildWorkerError(
|
|
633
|
-
"CODEX_APP_SERVER_TRANSPORT_ERROR",
|
|
634
|
-
error instanceof Error ? error.message : String(error),
|
|
635
|
-
),
|
|
636
|
-
raw: null,
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const client = this.client;
|
|
641
|
-
if (!client) {
|
|
642
|
-
return {
|
|
643
|
-
output: "codex app-server client 미초기화",
|
|
644
|
-
exitCode: CODEX_APP_SERVER_TRANSPORT_EXIT_CODE,
|
|
645
|
-
threadId: null,
|
|
646
|
-
sessionKey,
|
|
647
|
-
error: buildWorkerError(
|
|
648
|
-
"CODEX_APP_SERVER_TRANSPORT_ERROR",
|
|
649
|
-
"client missing",
|
|
650
|
-
),
|
|
651
|
-
raw: null,
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const outputParts = [];
|
|
656
|
-
let lastActivityAt = Date.now();
|
|
657
|
-
let activityLifecycle = null;
|
|
658
|
-
const unknownMethodsThisTurn = new Set();
|
|
659
|
-
let threadId =
|
|
660
|
-
typeof opts.threadId === "string" && opts.threadId ? opts.threadId : null;
|
|
661
|
-
let protocolMismatchWarned = false;
|
|
662
|
-
|
|
663
|
-
/** @type {((value: import('./interface.mjs').WorkerResult) => void) | null} */
|
|
664
|
-
let resolveResult = null;
|
|
665
|
-
const resultPromise = new Promise((resolve) => {
|
|
666
|
-
resolveResult = resolve;
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
const finish = (result) => {
|
|
670
|
-
if (!resolveResult) return;
|
|
671
|
-
const f = resolveResult;
|
|
672
|
-
resolveResult = null;
|
|
673
|
-
f(result);
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
// P1 #2 fail-fast: register ourselves so transport/child lifecycle errors
|
|
677
|
-
// can reject the in-flight turn immediately.
|
|
678
|
-
const inflightReject = (err) => {
|
|
679
|
-
finish({
|
|
680
|
-
output: outputParts.join(""),
|
|
681
|
-
exitCode: CODEX_APP_SERVER_TRANSPORT_EXIT_CODE,
|
|
682
|
-
threadId,
|
|
683
|
-
sessionKey,
|
|
684
|
-
error: buildWorkerError(
|
|
685
|
-
"CODEX_APP_SERVER_TRANSPORT_ERROR",
|
|
686
|
-
err instanceof Error ? err.message : String(err),
|
|
687
|
-
),
|
|
688
|
-
raw: null,
|
|
689
|
-
});
|
|
690
|
-
};
|
|
691
|
-
this._inflightRejectors.add(inflightReject);
|
|
692
|
-
|
|
693
|
-
const unsubscribers = [];
|
|
694
|
-
const offAll = () => {
|
|
695
|
-
while (unsubscribers.length) {
|
|
696
|
-
const off = unsubscribers.pop();
|
|
697
|
-
try {
|
|
698
|
-
off?.();
|
|
699
|
-
} catch {}
|
|
700
|
-
}
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
704
|
-
let timer = null;
|
|
705
|
-
|
|
706
|
-
try {
|
|
707
|
-
const pushPublish = (method, params) => {
|
|
708
|
-
if (!this.publishCallback) return;
|
|
709
|
-
const msg = this._buildPublishMessage(
|
|
710
|
-
method,
|
|
711
|
-
params,
|
|
712
|
-
threadId,
|
|
713
|
-
sessionKey,
|
|
714
|
-
);
|
|
715
|
-
// Fire-and-forget with 64-cap drop-oldest backpressure.
|
|
716
|
-
if (this._publishQueue.length >= this._publishMaxQueue) {
|
|
717
|
-
this._publishQueue.shift();
|
|
718
|
-
}
|
|
719
|
-
this._publishQueue.push(msg);
|
|
720
|
-
try {
|
|
721
|
-
const ret = this.publishCallback(msg);
|
|
722
|
-
if (ret && typeof ret.then === "function") {
|
|
723
|
-
ret.catch(() => {});
|
|
724
|
-
}
|
|
725
|
-
} catch {}
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
const handleNotification = (method, params) => {
|
|
729
|
-
// Any notification, including unknown and opt-out methods, proves the
|
|
730
|
-
// transport is live. Content filtering below remains unchanged.
|
|
731
|
-
lastActivityAt = Date.now();
|
|
732
|
-
activityLifecycle?.observe();
|
|
733
|
-
// AC5 defense: opt-out should already be filtered server-side.
|
|
734
|
-
if (OPT_OUT_METHODS.includes(method)) return;
|
|
735
|
-
|
|
736
|
-
if (!SUBSCRIBED_METHODS.has(method)) {
|
|
737
|
-
// AC6 / AC14 / AC16 — unknown method handling
|
|
738
|
-
unknownMethodsThisTurn.add(method);
|
|
739
|
-
if (!this._warnedUnknown.has(method)) {
|
|
740
|
-
this._warnedUnknown.add(method);
|
|
741
|
-
this._warn("unknown notification method", { method });
|
|
742
|
-
this._onUnknownMethod?.(method);
|
|
743
|
-
}
|
|
744
|
-
if (
|
|
745
|
-
!protocolMismatchWarned &&
|
|
746
|
-
unknownMethodsThisTurn.size > this.unknownMethodWarnThreshold
|
|
747
|
-
) {
|
|
748
|
-
protocolMismatchWarned = true;
|
|
749
|
-
this._warn("protocol_version_mismatch", {
|
|
750
|
-
count: unknownMethodsThisTurn.size,
|
|
751
|
-
threshold: this.unknownMethodWarnThreshold,
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Thread id can arrive on thread/started before thread/start resolves.
|
|
758
|
-
if (method === "thread/started") {
|
|
759
|
-
const tid = extractThreadIdFromStartedNotif(params);
|
|
760
|
-
if (tid && !threadId) threadId = tid;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// Agent text accumulation (AC3)
|
|
764
|
-
if (method === "item/agentMessage/delta") {
|
|
765
|
-
const delta = typeof params?.delta === "string" ? params.delta : "";
|
|
766
|
-
outputParts.push(delta);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
pushPublish(method, params);
|
|
770
|
-
|
|
771
|
-
if (method === "turn/completed") {
|
|
772
|
-
const status = params?.turn?.status;
|
|
773
|
-
if (status === "completed") {
|
|
774
|
-
finish({
|
|
775
|
-
output: outputParts.join(""),
|
|
776
|
-
exitCode: 0,
|
|
777
|
-
threadId,
|
|
778
|
-
sessionKey,
|
|
779
|
-
raw: { turn: params?.turn ?? null },
|
|
780
|
-
});
|
|
781
|
-
} else {
|
|
782
|
-
const msg =
|
|
783
|
-
params?.turn?.error?.message || `turn ${status || "unknown"}`;
|
|
784
|
-
finish({
|
|
785
|
-
output: outputParts.join(""),
|
|
786
|
-
exitCode: CODEX_APP_SERVER_EXECUTION_EXIT_CODE,
|
|
787
|
-
threadId,
|
|
788
|
-
sessionKey,
|
|
789
|
-
error: buildWorkerError("CODEX_APP_SERVER_EXECUTION_ERROR", msg),
|
|
790
|
-
raw: { turn: params?.turn ?? null },
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
if (method === "error") {
|
|
797
|
-
const msg = params?.message || "codex app-server error";
|
|
798
|
-
finish({
|
|
799
|
-
output: outputParts.join(""),
|
|
800
|
-
exitCode: CODEX_APP_SERVER_EXECUTION_EXIT_CODE,
|
|
801
|
-
threadId,
|
|
802
|
-
sessionKey,
|
|
803
|
-
error: buildWorkerError("CODEX_APP_SERVER_EXECUTION_ERROR", msg),
|
|
804
|
-
raw: { error: params ?? null },
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
// NOTE: JsonRpcStdioClient wildcard subscribers receive (params, method).
|
|
810
|
-
// Match that contract here; targeted subscribers (cb(params)) are unchanged.
|
|
811
|
-
const catchAll = (params, method) => handleNotification(method, params);
|
|
812
|
-
unsubscribers.push(client.onNotification("*", catchAll));
|
|
813
|
-
|
|
814
|
-
// Send thread/start (no prompt) then turn/start (prompt)
|
|
815
|
-
let threadStartResponse;
|
|
816
|
-
try {
|
|
817
|
-
threadStartResponse = await client.request(
|
|
818
|
-
"thread/start",
|
|
819
|
-
buildThreadStartParams(opts),
|
|
820
|
-
this.bootstrapTimeoutMs,
|
|
821
|
-
);
|
|
822
|
-
} catch (error) {
|
|
823
|
-
return {
|
|
824
|
-
output: error instanceof Error ? error.message : String(error),
|
|
825
|
-
exitCode: CODEX_APP_SERVER_TRANSPORT_EXIT_CODE,
|
|
826
|
-
threadId,
|
|
827
|
-
sessionKey,
|
|
828
|
-
error: buildWorkerError(
|
|
829
|
-
"CODEX_APP_SERVER_TRANSPORT_ERROR",
|
|
830
|
-
error instanceof Error ? error.message : String(error),
|
|
831
|
-
),
|
|
832
|
-
raw: null,
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
const respThreadId =
|
|
836
|
-
extractThreadIdFromStartResponse(threadStartResponse);
|
|
837
|
-
if (respThreadId && !threadId) threadId = respThreadId;
|
|
838
|
-
if (!threadId) {
|
|
839
|
-
return {
|
|
840
|
-
output: "thread id를 추출하지 못했습니다.",
|
|
841
|
-
exitCode: CODEX_APP_SERVER_EXECUTION_EXIT_CODE,
|
|
842
|
-
threadId: null,
|
|
843
|
-
sessionKey,
|
|
844
|
-
error: buildWorkerError(
|
|
845
|
-
"CODEX_APP_SERVER_EXECUTION_ERROR",
|
|
846
|
-
"thread id missing",
|
|
847
|
-
),
|
|
848
|
-
raw: { threadStart: threadStartResponse },
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
this.activeThreadId = threadId;
|
|
852
|
-
|
|
853
|
-
try {
|
|
854
|
-
await client.request(
|
|
855
|
-
"turn/start",
|
|
856
|
-
buildTurnStartParams(threadId, prompt),
|
|
857
|
-
this.bootstrapTimeoutMs,
|
|
858
|
-
);
|
|
859
|
-
} catch (error) {
|
|
860
|
-
return {
|
|
861
|
-
output: error instanceof Error ? error.message : String(error),
|
|
862
|
-
exitCode: CODEX_APP_SERVER_TRANSPORT_EXIT_CODE,
|
|
863
|
-
threadId,
|
|
864
|
-
sessionKey,
|
|
865
|
-
error: buildWorkerError(
|
|
866
|
-
"CODEX_APP_SERVER_TRANSPORT_ERROR",
|
|
867
|
-
error instanceof Error ? error.message : String(error),
|
|
868
|
-
),
|
|
869
|
-
raw: null,
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// In activity mode timeoutMs is the inactivity window. Notifications
|
|
874
|
-
// re-arm the lifecycle; hardCeilingMs remains the leak-prevention bound.
|
|
875
|
-
const activityEnabled = isActivityLifecycleEnabled();
|
|
876
|
-
const timeoutMs = Number.isFinite(opts.timeoutMs)
|
|
877
|
-
? opts.timeoutMs
|
|
878
|
-
: activityEnabled
|
|
879
|
-
? resolveStallInterventionMs()
|
|
880
|
-
: DEFAULT_CODEX_APP_SERVER_EXECUTION_TIMEOUT_MS;
|
|
881
|
-
const hardCeilingMs = Number.isFinite(opts.hardCeilingMs)
|
|
882
|
-
? opts.hardCeilingMs
|
|
883
|
-
: resolveHardCeilingMs();
|
|
884
|
-
// Capture current child so a stale timer (post-result) cannot SIGTERM a
|
|
885
|
-
// later reused worker's child process.
|
|
886
|
-
const capturedChild = this.child;
|
|
887
|
-
activityLifecycle = createActivityLifecycle({
|
|
888
|
-
enabled: activityEnabled,
|
|
889
|
-
interventionMs: timeoutMs,
|
|
890
|
-
hardCeilingMs,
|
|
891
|
-
onIntervene: async (context) => {
|
|
892
|
-
if (this._interveneFn) {
|
|
893
|
-
const result = await this._interveneFn({
|
|
894
|
-
threadId,
|
|
895
|
-
client,
|
|
896
|
-
lastActivityAt,
|
|
897
|
-
...context,
|
|
898
|
-
});
|
|
899
|
-
return result === true || result === "resolved";
|
|
900
|
-
}
|
|
901
|
-
return false;
|
|
902
|
-
},
|
|
903
|
-
});
|
|
904
|
-
const timeoutPromise = new Promise((resolve) => {
|
|
905
|
-
const fire = (reason) => {
|
|
906
|
-
this._warn("execution timeout", { timeoutMs, reason });
|
|
907
|
-
try {
|
|
908
|
-
capturedChild?.kill?.("SIGTERM");
|
|
909
|
-
} catch {}
|
|
910
|
-
resolve({
|
|
911
|
-
output: outputParts.join(""),
|
|
912
|
-
exitCode: CODEX_APP_SERVER_TIMEOUT_EXIT_CODE,
|
|
913
|
-
threadId,
|
|
914
|
-
sessionKey,
|
|
915
|
-
error: buildWorkerError(
|
|
916
|
-
"CODEX_APP_SERVER_TIMEOUT",
|
|
917
|
-
`codex app-server timeout (${timeoutMs}ms)`,
|
|
918
|
-
),
|
|
919
|
-
raw: null,
|
|
920
|
-
});
|
|
921
|
-
};
|
|
922
|
-
const arm = () => {
|
|
923
|
-
timer = setTimeout(async () => {
|
|
924
|
-
if (!activityEnabled) return fire("timeout");
|
|
925
|
-
const reason = await activityLifecycle.check({ threadId, client });
|
|
926
|
-
if (reason) return fire(reason);
|
|
927
|
-
arm();
|
|
928
|
-
}, timeoutMs);
|
|
929
|
-
};
|
|
930
|
-
arm();
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
const result = await Promise.race([resultPromise, timeoutPromise]);
|
|
934
|
-
return result;
|
|
935
|
-
} finally {
|
|
936
|
-
if (timer !== null) {
|
|
937
|
-
clearTimeout(timer);
|
|
938
|
-
timer = null;
|
|
939
|
-
}
|
|
940
|
-
offAll();
|
|
941
|
-
this._inflightRejectors.delete(inflightReject);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
/**
|
|
946
|
-
* Graceful shutdown — best-effort thread/unsubscribe (request, with timeout),
|
|
947
|
-
* close JSON-RPC client, SIGTERM the child then SIGKILL after 1 s.
|
|
948
|
-
*
|
|
949
|
-
* P1 #1 wire framing: `thread/unsubscribe` is sent as a **request** per the
|
|
950
|
-
* OpenAI App Server API overview (was notification in the original PR #86).
|
|
951
|
-
* P1 #2 lifecycle: we wait up to `UNSUBSCRIBE_DEADLINE_MS` for the response
|
|
952
|
-
* before proceeding to SIGTERM so the server can flush unload work.
|
|
953
|
-
* @returns {Promise<void>}
|
|
954
|
-
*/
|
|
955
|
-
async stop() {
|
|
956
|
-
this.ready = false;
|
|
957
|
-
const child = this.child;
|
|
958
|
-
const client = this.client;
|
|
959
|
-
const activeThread = this.activeThreadId;
|
|
960
|
-
this.child = null;
|
|
961
|
-
this.client = null;
|
|
962
|
-
this.activeThreadId = null;
|
|
963
|
-
|
|
964
|
-
if (this._detachChildLifecycle) {
|
|
965
|
-
try {
|
|
966
|
-
this._detachChildLifecycle();
|
|
967
|
-
} catch {}
|
|
968
|
-
this._detachChildLifecycle = null;
|
|
969
|
-
}
|
|
970
|
-
// Reject any still-registered in-flight execute() with a generic transport
|
|
971
|
-
// error so callers don't hang past stop().
|
|
972
|
-
this._drainInflight(
|
|
973
|
-
new CodexAppServerTransportError("codex app-server stop() 호출됨", {
|
|
974
|
-
stderr: this.serverStderr,
|
|
975
|
-
}),
|
|
976
|
-
);
|
|
977
|
-
|
|
978
|
-
if (this._rejectBootstrap) {
|
|
979
|
-
try {
|
|
980
|
-
this._rejectBootstrap(
|
|
981
|
-
new CodexAppServerTransportError("codex app-server stop() 호출됨"),
|
|
982
|
-
);
|
|
983
|
-
} catch {}
|
|
984
|
-
this._rejectBootstrap = null;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
if (client) {
|
|
988
|
-
if (activeThread) {
|
|
989
|
-
try {
|
|
990
|
-
if (client.isOpen()) {
|
|
991
|
-
// Enter `closing` state so the EOF that follows `close()` doesn't
|
|
992
|
-
// trip the fail-fast path. Send unsubscribe as a request and race
|
|
993
|
-
// it against a hard deadline — some servers (and test fakes) may
|
|
994
|
-
// never respond, and we must not hang stop().
|
|
995
|
-
const unsubPromise = client
|
|
996
|
-
.request(
|
|
997
|
-
"thread/unsubscribe",
|
|
998
|
-
{ threadId: activeThread },
|
|
999
|
-
UNSUBSCRIBE_DEADLINE_MS,
|
|
1000
|
-
)
|
|
1001
|
-
.catch(() => {
|
|
1002
|
-
/* best-effort; fall through to close + SIGTERM below */
|
|
1003
|
-
});
|
|
1004
|
-
client.close("closing");
|
|
1005
|
-
const deadline = new Promise((resolve) => {
|
|
1006
|
-
setTimeout(resolve, UNSUBSCRIBE_DEADLINE_MS);
|
|
1007
|
-
});
|
|
1008
|
-
await Promise.race([unsubPromise, deadline]);
|
|
1009
|
-
}
|
|
1010
|
-
} catch {}
|
|
1011
|
-
}
|
|
1012
|
-
try {
|
|
1013
|
-
client.close();
|
|
1014
|
-
} catch {}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
if (child) {
|
|
1018
|
-
try {
|
|
1019
|
-
child.stdin?.end?.();
|
|
1020
|
-
} catch {}
|
|
1021
|
-
try {
|
|
1022
|
-
if (child.exitCode === null && !child.killed) child.kill("SIGTERM");
|
|
1023
|
-
} catch {}
|
|
1024
|
-
if (child.exitCode === null && !child.killed) {
|
|
1025
|
-
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
1026
|
-
try {
|
|
1027
|
-
if (child.exitCode === null && !child.killed) child.kill("SIGKILL");
|
|
1028
|
-
} catch {}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
/**
|
|
1034
|
-
* Build the `agent.progress` publish envelope for a codex notification.
|
|
1035
|
-
* Compatible with `hub/bridge.mjs:buildPublishBody()` so the same object can
|
|
1036
|
-
* be handed to `requestJson('/bridge/publish', { body })` or a router shim.
|
|
1037
|
-
* @param {string} method
|
|
1038
|
-
* @param {unknown} params
|
|
1039
|
-
* @param {string | null} threadId
|
|
1040
|
-
* @param {string | null} sessionKey
|
|
1041
|
-
*/
|
|
1042
|
-
_buildPublishMessage(method, params, threadId, sessionKey) {
|
|
1043
|
-
const kind = KIND_MAP[method] || "unknown";
|
|
1044
|
-
const data = this.redactSensitive ? redactDeep(params) : params;
|
|
1045
|
-
return {
|
|
1046
|
-
from: this.workerId,
|
|
1047
|
-
to: "topic:agent.progress",
|
|
1048
|
-
topic: "agent.progress",
|
|
1049
|
-
type: "event",
|
|
1050
|
-
payload: {
|
|
1051
|
-
type: "agent.progress",
|
|
1052
|
-
version: 1,
|
|
1053
|
-
agentId: this.workerId,
|
|
1054
|
-
sessionKey: sessionKey || "",
|
|
1055
|
-
threadId: threadId || "",
|
|
1056
|
-
kind,
|
|
1057
|
-
method,
|
|
1058
|
-
timestamp: this._monotonicNow(),
|
|
1059
|
-
data,
|
|
1060
|
-
},
|
|
1061
|
-
};
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
export function createCodexAppServerWorker(options = {}) {
|
|
1066
|
-
return new CodexAppServerWorker(options);
|
|
1067
|
-
}
|