@gh-symphony/cli 0.0.19 → 0.0.21

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.
@@ -1,450 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ getCodexObservabilityEventName,
4
+ launchCodexAppServer,
5
+ loadLauncherEnvironment,
6
+ normalizeCodexRuntimeEvents,
7
+ prepareCodexRuntimePlan,
8
+ resolveLocalRuntimeLaunchConfig
9
+ } from "./chunk-A67CMOYE.js";
10
+ import {
11
+ DEFAULT_AGENT_INPUT_REQUIRED_REASON,
3
12
  classifySessionExit,
13
+ formatClaudePreflightText,
14
+ isClaudeRuntimeCommand,
4
15
  parseWorkflowMarkdown,
5
- readEnvFile
6
- } from "./chunk-M3IFVLQS.js";
7
-
8
- // ../worker/dist/index.js
9
- import { spawn as spawn2 } from "child_process";
10
- import { readFile as readFile2 } from "fs/promises";
11
- import { join as join3 } from "path";
16
+ resolveClaudeCommandBinary,
17
+ resolveWorkflowRuntimeCommand,
18
+ runClaudePreflight
19
+ } from "./chunk-QEONJ5DZ.js";
12
20
 
13
- // ../runtime-codex/dist/runtime.js
21
+ // ../worker/src/index.ts
14
22
  import { spawn } from "child_process";
15
- import { copyFile, mkdir, writeFile } from "fs/promises";
16
- import { join } from "path";
17
- import { homedir } from "os";
18
- import { fileURLToPath } from "url";
19
- var DEFAULT_GITHUB_GRAPHQL_API_URL = "https://api.github.com/graphql";
20
- var DEFAULT_GITHUB_GIT_HOST = "github.com";
21
- var DEFAULT_GITHUB_GIT_USERNAME = "x-access-token";
22
- var AgentRuntimeResolutionError = class extends Error {
23
- };
24
- function createGitHubGraphQLToolDefinition(config) {
25
- return {
26
- name: "github_graphql",
27
- description: "Execute GitHub GraphQL queries for the active workspace so the agent can mutate project and issue state directly.",
28
- command: "node",
29
- args: [fileURLToPath(new URL("./github-graphql-mcp-server.js", import.meta.url))],
30
- env: {
31
- GITHUB_GRAPHQL_API_URL: config.githubGraphqlApiUrl ?? DEFAULT_GITHUB_GRAPHQL_API_URL,
32
- ...config.githubToken ? {
33
- GITHUB_GRAPHQL_TOKEN: config.githubToken
34
- } : {},
35
- ...config.githubTokenBrokerUrl ? {
36
- GITHUB_TOKEN_BROKER_URL: config.githubTokenBrokerUrl
37
- } : {},
38
- ...config.githubTokenBrokerSecret ? {
39
- GITHUB_TOKEN_BROKER_SECRET: config.githubTokenBrokerSecret
40
- } : {},
41
- ...config.githubTokenCachePath ? {
42
- GITHUB_TOKEN_CACHE_PATH: config.githubTokenCachePath
43
- } : {},
44
- ...config.githubProjectId ? {
45
- GITHUB_PROJECT_ID: config.githubProjectId
46
- } : {}
47
- },
48
- inputSchema: {
49
- type: "object",
50
- properties: {
51
- query: {
52
- type: "string",
53
- description: "GraphQL query or mutation document."
54
- },
55
- variables: {
56
- type: "object",
57
- description: "Variables for the GraphQL document."
58
- },
59
- operationName: {
60
- type: "string",
61
- description: "Optional GraphQL operation name."
62
- }
63
- },
64
- required: ["query"],
65
- additionalProperties: false
66
- }
67
- };
68
- }
69
- function buildCodexRuntimePlan(config) {
70
- const tool = createGitHubGraphQLToolDefinition(config);
71
- const gitCredentialHelper = createGitCredentialHelperEnvironment(config);
72
- const shellCmd = (() => {
73
- const cmd = config.agentCommand ?? "codex app-server";
74
- return cmd.startsWith("bash -lc ") ? cmd.slice("bash -lc ".length) : cmd;
75
- })();
76
- return {
77
- cwd: config.workingDirectory,
78
- command: "bash",
79
- args: ["-lc", shellCmd],
80
- env: {
81
- ...process.env,
82
- ...config.extraEnv,
83
- ...config.agentEnv,
84
- CODEX_PROJECT_ID: config.projectId,
85
- GITHUB_PROJECT_ID: config.githubProjectId ?? "",
86
- GITHUB_GRAPHQL_TOOL_NAME: tool.name,
87
- GITHUB_GRAPHQL_TOOL_COMMAND: [tool.command, ...tool.args].join(" "),
88
- // Point codex to an isolated config dir so personal MCPs (playwright,
89
- // chrome-devtools, context7, etc.) from the operator's ~/.codex/config.toml
90
- // are not loaded and do not confuse the implementation agent.
91
- CODEX_HOME: join(config.workingDirectory, ".codex-agent"),
92
- ...gitCredentialHelper,
93
- ...tool.env
94
- },
95
- tools: [tool],
96
- resumeThreadId: config.resumeThreadId?.trim() || null
97
- };
98
- }
99
- function launchCodexAppServer(plan, spawnImpl = spawn) {
100
- return spawnImpl(plan.command, plan.args, {
101
- cwd: plan.cwd,
102
- env: plan.env,
103
- stdio: "pipe"
104
- });
105
- }
106
- async function prepareCodexRuntimePlan(config, dependencies = {}) {
107
- const agentEnv = await resolveAgentRuntimeEnvironment(config, dependencies);
108
- const codexHomeDir = join(config.workingDirectory, ".codex-agent");
109
- const mkdirImpl = dependencies.mkdirImpl ?? mkdir;
110
- await mkdirImpl(codexHomeDir, { recursive: true });
111
- const writeFileImpl = dependencies.writeFileImpl ?? writeFile;
112
- await writeFileImpl(join(codexHomeDir, "config.toml"), "# Isolated agent config \u2014 no personal MCP servers\n", "utf8");
113
- const realCodexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
114
- const copyFileImpl = dependencies.copyFileImpl ?? copyFile;
115
- try {
116
- await copyFileImpl(join(realCodexHome, "auth.json"), join(codexHomeDir, "auth.json"));
117
- } catch {
118
- }
119
- return buildCodexRuntimePlan({
120
- ...config,
121
- agentEnv
122
- });
123
- }
124
- function createGitCredentialHelperEnvironment(config) {
125
- return {
126
- GITHUB_GIT_HOST: DEFAULT_GITHUB_GIT_HOST,
127
- GITHUB_GIT_USERNAME: DEFAULT_GITHUB_GIT_USERNAME,
128
- GIT_TERMINAL_PROMPT: "0",
129
- GIT_CONFIG_COUNT: "1",
130
- GIT_CONFIG_KEY_0: "credential.helper",
131
- GIT_CONFIG_VALUE_0: `!node ${fileURLToPath(new URL("./git-credential-helper.js", import.meta.url))}`,
132
- ...config.githubToken ? {
133
- GITHUB_GRAPHQL_TOKEN: config.githubToken
134
- } : {},
135
- ...config.githubTokenBrokerUrl ? {
136
- GITHUB_TOKEN_BROKER_URL: config.githubTokenBrokerUrl
137
- } : {},
138
- ...config.githubTokenBrokerSecret ? {
139
- GITHUB_TOKEN_BROKER_SECRET: config.githubTokenBrokerSecret
140
- } : {},
141
- ...config.githubTokenCachePath ? {
142
- GITHUB_TOKEN_CACHE_PATH: config.githubTokenCachePath
143
- } : {}
144
- };
145
- }
146
- async function resolveAgentRuntimeEnvironment(config, dependencies = {}) {
147
- if (config.agentEnv) {
148
- return config.agentEnv;
149
- }
150
- if (!config.agentCredentialBrokerUrl || !config.agentCredentialBrokerSecret) {
151
- return {};
152
- }
153
- const fetchImpl = dependencies.fetchImpl ?? fetch;
154
- const response = await fetchImpl(config.agentCredentialBrokerUrl, {
155
- method: "POST",
156
- headers: {
157
- accept: "application/json",
158
- authorization: `Bearer ${config.agentCredentialBrokerSecret}`
159
- }
160
- });
161
- const payload = await response.json();
162
- if (!response.ok || !payload.env || Object.keys(payload.env).length === 0) {
163
- throw new AgentRuntimeResolutionError(payload.error ?? `Agent credential broker request failed with status ${response.status}.`);
164
- }
165
- if (config.agentCredentialCachePath) {
166
- const writeFileImpl = dependencies.writeFileImpl ?? writeFile;
167
- await writeFileImpl(config.agentCredentialCachePath, JSON.stringify(payload), "utf8");
168
- }
169
- return payload.env;
170
- }
171
-
172
- // ../runtime-codex/dist/launcher.js
173
- import { dirname, resolve } from "path";
174
- import { fileURLToPath as fileURLToPath2 } from "url";
175
- var LocalRuntimeLauncherError = class extends Error {
176
- };
177
- function resolveLocalRuntimeLaunchConfig(env = process.env) {
178
- const projectId = env.PROJECT_ID ?? env.CODEX_PROJECT_ID;
179
- const workingDirectory = env.WORKING_DIRECTORY;
180
- if (!projectId) {
181
- throw new LocalRuntimeLauncherError("PROJECT_ID or CODEX_PROJECT_ID is required.");
182
- }
183
- if (!workingDirectory) {
184
- throw new LocalRuntimeLauncherError("WORKING_DIRECTORY is required.");
185
- }
186
- return {
187
- projectId,
188
- workingDirectory,
189
- githubToken: env.GITHUB_GRAPHQL_TOKEN,
190
- githubTokenBrokerUrl: env.GITHUB_TOKEN_BROKER_URL,
191
- githubTokenBrokerSecret: env.GITHUB_TOKEN_BROKER_SECRET,
192
- githubTokenCachePath: env.GITHUB_TOKEN_CACHE_PATH,
193
- agentEnv: readDirectAgentEnvironment(env),
194
- agentCredentialBrokerUrl: env.AGENT_CREDENTIAL_BROKER_URL,
195
- agentCredentialBrokerSecret: env.AGENT_CREDENTIAL_BROKER_SECRET,
196
- agentCredentialCachePath: env.AGENT_CREDENTIAL_CACHE_PATH,
197
- githubProjectId: env.GITHUB_PROJECT_ID,
198
- githubGraphqlApiUrl: env.GITHUB_GRAPHQL_API_URL,
199
- agentCommand: env.SYMPHONY_AGENT_COMMAND,
200
- resumeThreadId: env.SYMPHONY_RESUME_THREAD_ID
201
- };
202
- }
203
- async function runLocalRuntimeLauncher(env = process.env) {
204
- const launcherEnv2 = loadLauncherEnvironment(env);
205
- const config = resolveLocalRuntimeLaunchConfig(launcherEnv2);
206
- const plan = await prepareCodexRuntimePlan(config);
207
- emitLaunchSummary(config);
208
- const child = launchCodexAppServer(plan);
209
- process.stdout.write(`[worker] codex app-server started (pid: ${child.pid ?? "unknown"})
210
- `);
211
- child.stdout?.pipe(process.stdout);
212
- child.stderr?.pipe(process.stderr);
213
- return await waitForChildProcess(child);
214
- }
215
- function loadLauncherEnvironment(env = process.env, cwd = process.cwd()) {
216
- const mergedEnv = {
217
- ...readEnvFile(resolve(dirname(fileURLToPath2(import.meta.url)), "..", ".env")),
218
- ...readEnvFile(resolve(cwd, ".env")),
219
- ...env
220
- };
221
- return mergedEnv;
222
- }
223
- function readDirectAgentEnvironment(env) {
224
- const agentEnv = {};
225
- for (const key of [
226
- "OPENAI_API_KEY",
227
- "OPENAI_BASE_URL",
228
- "OPENAI_ORG_ID",
229
- "OPENAI_PROJECT"
230
- ]) {
231
- const value = env[key];
232
- if (value) {
233
- agentEnv[key] = value;
234
- }
235
- }
236
- return Object.keys(agentEnv).length ? agentEnv : void 0;
237
- }
238
- function waitForChildProcess(child) {
239
- return new Promise((resolve2, reject) => {
240
- child.once("error", reject);
241
- child.once("exit", (code, signal) => {
242
- if (signal) {
243
- reject(new LocalRuntimeLauncherError(`codex app-server exited on ${signal}.`));
244
- return;
245
- }
246
- resolve2(code ?? 0);
247
- });
248
- });
249
- }
250
- async function main() {
251
- const exitCode = await runLocalRuntimeLauncher(process.env);
252
- process.exitCode = exitCode;
253
- }
254
- if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) {
255
- main().catch((error) => {
256
- const message = error instanceof Error ? error.message : "Unknown error";
257
- process.stderr.write(`${message}
258
- `);
259
- process.exitCode = 1;
260
- });
261
- }
262
- function emitLaunchSummary(config) {
263
- const githubAuthMode = config.githubToken ? "direct token" : config.githubTokenBrokerUrl && config.githubTokenBrokerSecret ? "broker" : "missing";
264
- const agentAuthMode = config.agentEnv?.OPENAI_API_KEY ? "direct env" : config.agentCredentialBrokerUrl && config.agentCredentialBrokerSecret ? "broker" : "local codex auth or inherited environment";
265
- process.stdout.write([
266
- "[worker] starting local codex runtime",
267
- `[worker] project: ${config.projectId}`,
268
- `[worker] cwd: ${config.workingDirectory}`,
269
- `[worker] github project: ${config.githubProjectId ?? "(unset)"}`,
270
- `[worker] github auth: ${githubAuthMode}`,
271
- `[worker] agent auth: ${agentAuthMode}`,
272
- "[worker] note: codex app-server does not proactively read GitHub issues.",
273
- "[worker] note: it waits for a client request or tool invocation."
274
- ].join("\n") + "\n");
275
- }
276
-
277
- // ../runtime-codex/dist/github-graphql-tool.js
278
- import { readFile, writeFile as writeFile2 } from "fs/promises";
279
- var DEFAULT_GITHUB_GRAPHQL_API_URL2 = "https://api.github.com/graphql";
280
- var TOKEN_REUSE_WINDOW_MS = 60 * 1e3;
281
- async function executeGitHubGraphQL(invocation, config, fetchImpl = fetch) {
282
- const token = await resolveGitHubGraphQLToken(config, {
283
- fetchImpl
284
- });
285
- const response = await fetchImpl(config.apiUrl ?? DEFAULT_GITHUB_GRAPHQL_API_URL2, {
286
- method: "POST",
287
- headers: {
288
- "content-type": "application/json",
289
- authorization: `Bearer ${token}`
290
- },
291
- body: JSON.stringify(invocation)
292
- });
293
- const payload = await response.json();
294
- if (!response.ok) {
295
- throw new Error(`GitHub GraphQL request failed with status ${response.status}: ${JSON.stringify(payload)}`);
296
- }
297
- if (payload.errors?.length) {
298
- throw new Error(payload.errors.map((error) => error.message).join("; "));
299
- }
300
- return payload;
301
- }
302
- async function resolveGitHubGraphQLToken(config, dependencies = {}) {
303
- if (config.token) {
304
- return config.token;
305
- }
306
- if (!config.tokenBrokerUrl || !config.tokenBrokerSecret) {
307
- throw new Error("Either GITHUB_GRAPHQL_TOKEN or the runtime token broker configuration is required.");
308
- }
309
- const now = dependencies.now ?? /* @__PURE__ */ new Date();
310
- const readFileImpl = dependencies.readFileImpl ?? readFile;
311
- const writeFileImpl = dependencies.writeFileImpl ?? writeFile2;
312
- const cachedToken = config.tokenCachePath ? await readCachedToken(config.tokenCachePath, readFileImpl) : null;
313
- if (cachedToken && cachedToken.expiresAt.getTime() - now.getTime() > TOKEN_REUSE_WINDOW_MS) {
314
- return cachedToken.token;
315
- }
316
- const fetchImpl = dependencies.fetchImpl ?? fetch;
317
- const response = await fetchImpl(config.tokenBrokerUrl, {
318
- method: "POST",
319
- headers: {
320
- accept: "application/json",
321
- authorization: `Bearer ${config.tokenBrokerSecret}`
322
- }
323
- });
324
- const payload = await response.json();
325
- if (!response.ok || !payload.token || !payload.expiresAt) {
326
- throw new Error(payload.error ?? `Runtime token broker request failed with status ${response.status}.`);
327
- }
328
- if (config.tokenCachePath) {
329
- await writeFileImpl(config.tokenCachePath, JSON.stringify(payload), "utf8");
330
- }
331
- return payload.token;
332
- }
333
- async function readStdin() {
334
- const chunks = [];
335
- for await (const chunk of process.stdin) {
336
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
337
- }
338
- return Buffer.concat(chunks).toString("utf8");
339
- }
340
- async function main2() {
341
- const rawInput = await readStdin();
342
- const invocation = JSON.parse(rawInput);
343
- const result = await executeGitHubGraphQL(invocation, {
344
- token: process.env.GITHUB_GRAPHQL_TOKEN,
345
- apiUrl: process.env.GITHUB_GRAPHQL_API_URL,
346
- tokenBrokerUrl: process.env.GITHUB_TOKEN_BROKER_URL,
347
- tokenBrokerSecret: process.env.GITHUB_TOKEN_BROKER_SECRET,
348
- tokenCachePath: process.env.GITHUB_TOKEN_CACHE_PATH
349
- });
350
- process.stdout.write(`${JSON.stringify(result)}
351
- `);
352
- }
353
- if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) {
354
- main2().catch((error) => {
355
- const message = error instanceof Error ? error.message : "Unknown error";
356
- process.stderr.write(`${message}
357
- `);
358
- process.exitCode = 1;
359
- });
360
- }
361
- async function readCachedToken(path, readFileImpl) {
362
- try {
363
- const payload = JSON.parse(await readFileImpl(path, "utf8"));
364
- if (!payload.token || !payload.expiresAt) {
365
- return null;
366
- }
367
- return {
368
- token: payload.token,
369
- expiresAt: new Date(payload.expiresAt)
370
- };
371
- } catch {
372
- return null;
373
- }
374
- }
375
-
376
- // ../runtime-codex/dist/git-credential-helper.js
377
- var DEFAULT_GITHUB_GIT_HOST2 = "github.com";
378
- var DEFAULT_GITHUB_GIT_USERNAME2 = "x-access-token";
379
- async function resolveGitCredential(request, config, fetchImpl = fetch) {
380
- const requestHost = request.host?.trim();
381
- const requestProtocol = request.protocol?.trim();
382
- if (!requestHost || requestProtocol && requestProtocol !== "https") {
383
- return "";
384
- }
385
- const expectedHost = normalizeGitHost(config.gitHost ?? DEFAULT_GITHUB_GIT_HOST2);
386
- if (normalizeGitHost(requestHost) !== expectedHost) {
387
- return "";
388
- }
389
- const token = await resolveGitHubGraphQLToken(config, {
390
- fetchImpl
391
- });
392
- return formatGitCredentialResponse({
393
- protocol: requestProtocol || "https",
394
- host: requestHost,
395
- username: config.gitUsername ?? DEFAULT_GITHUB_GIT_USERNAME2,
396
- password: token
397
- });
398
- }
399
- function parseGitCredentialRequest(rawInput) {
400
- return rawInput.split("\n").map((line) => line.trim()).filter(Boolean).reduce((request, line) => {
401
- const separatorIndex = line.indexOf("=");
402
- if (separatorIndex === -1) {
403
- return request;
404
- }
405
- const key = line.slice(0, separatorIndex);
406
- const value = line.slice(separatorIndex + 1);
407
- request[key] = value;
408
- return request;
409
- }, {});
410
- }
411
- function formatGitCredentialResponse(value) {
412
- return `${Object.entries(value).map(([key, entry]) => `${key}=${entry}`).join("\n")}
413
-
414
- `;
415
- }
416
- async function readStdin2() {
417
- const chunks = [];
418
- for await (const chunk of process.stdin) {
419
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
420
- }
421
- return Buffer.concat(chunks).toString("utf8");
422
- }
423
- async function main3() {
424
- const request = parseGitCredentialRequest(await readStdin2());
425
- const response = await resolveGitCredential(request, {
426
- token: process.env.GITHUB_GRAPHQL_TOKEN,
427
- tokenBrokerUrl: process.env.GITHUB_TOKEN_BROKER_URL,
428
- tokenBrokerSecret: process.env.GITHUB_TOKEN_BROKER_SECRET,
429
- tokenCachePath: process.env.GITHUB_TOKEN_CACHE_PATH,
430
- gitHost: process.env.GITHUB_GIT_HOST,
431
- gitUsername: process.env.GITHUB_GIT_USERNAME
432
- });
433
- process.stdout.write(response);
434
- }
435
- if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) {
436
- main3().catch((error) => {
437
- const message = error instanceof Error ? error.message : "Unknown error";
438
- process.stderr.write(`${message}
439
- `);
440
- process.exitCode = 1;
441
- });
442
- }
443
- function normalizeGitHost(host) {
444
- return host.trim().toLowerCase();
445
- }
23
+ import { readFile } from "fs/promises";
24
+ import { join as join2 } from "path";
446
25
 
447
- // ../worker/dist/execution-phase.js
26
+ // ../worker/src/execution-phase.ts
448
27
  function resolveInitialExecutionPhase(input) {
449
28
  const { issueState, blockerCheckStates, activeStates } = input;
450
29
  if (!issueState) {
@@ -474,7 +53,7 @@ function resolveFinalExecutionPhase(input) {
474
53
  return resolvePausedExecutionPhase(input.currentPhase) ?? input.currentPhase;
475
54
  }
476
55
 
477
- // ../worker/dist/codex-policy.js
56
+ // ../worker/src/codex-policy.ts
478
57
  function resolveCodexPolicySettings(env) {
479
58
  return {
480
59
  approvalPolicy: env.SYMPHONY_APPROVAL_POLICY || "never",
@@ -483,7 +62,7 @@ function resolveCodexPolicySettings(env) {
483
62
  };
484
63
  }
485
64
 
486
- // ../worker/dist/convergence-detection.js
65
+ // ../worker/src/convergence-detection.ts
487
66
  import { spawnSync } from "child_process";
488
67
  var DEFAULT_MAX_NONPRODUCTIVE_TURNS = 3;
489
68
  function resolveMaxNonProductiveTurns(env) {
@@ -492,10 +71,14 @@ function resolveMaxNonProductiveTurns(env) {
492
71
  return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_NONPRODUCTIVE_TURNS;
493
72
  }
494
73
  function captureTurnWorkspaceSnapshot(cwd) {
495
- const result = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
496
- cwd,
497
- encoding: "utf8"
498
- });
74
+ const result = spawnSync(
75
+ "git",
76
+ ["status", "--porcelain=v1", "--untracked-files=all"],
77
+ {
78
+ cwd,
79
+ encoding: "utf8"
80
+ }
81
+ );
499
82
  if (result.status !== 0) {
500
83
  return {
501
84
  fingerprint: null,
@@ -541,7 +124,7 @@ function normalizeError(value) {
541
124
  return normalized.length > 0 ? normalized : null;
542
125
  }
543
126
 
544
- // ../worker/dist/run-phase.js
127
+ // ../worker/src/run-phase.ts
545
128
  var TERMINAL_RUN_PHASES = /* @__PURE__ */ new Set([
546
129
  "succeeded",
547
130
  "failed",
@@ -556,111 +139,26 @@ function resolveExitRunPhase(currentRunPhase, exit) {
556
139
  return exit.code === 0 && !exit.signal ? "succeeded" : "failed";
557
140
  }
558
141
 
559
- // ../worker/dist/session-budget.js
560
- function resolveSessionBudgetState(env) {
561
- return {
562
- cumulativeTurnCount: parseNonNegativeInteger(env.SYMPHONY_CUMULATIVE_TURN_COUNT),
563
- tokenUsageBaseline: {
564
- inputTokens: parseNonNegativeInteger(env.SYMPHONY_CUMULATIVE_INPUT_TOKENS),
565
- outputTokens: parseNonNegativeInteger(env.SYMPHONY_CUMULATIVE_OUTPUT_TOKENS),
566
- totalTokens: parseNonNegativeInteger(env.SYMPHONY_CUMULATIVE_TOTAL_TOKENS)
567
- },
568
- sessionStartedAt: normalizeTimestamp(env.SYMPHONY_SESSION_STARTED_AT),
569
- globalMaxTurns: parsePositiveInteger(env.SYMPHONY_GLOBAL_MAX_TURNS),
570
- maxTokens: parsePositiveInteger(env.SYMPHONY_MAX_TOKENS),
571
- sessionTimeoutMs: parsePositiveInteger(env.SYMPHONY_SESSION_TIMEOUT_MS)
572
- };
573
- }
574
- function resolveBudgetExceededReason(budget, currentSessionTurnCount, currentTokenUsage, now) {
575
- const totalTurns = budget.cumulativeTurnCount + currentSessionTurnCount;
576
- if (budget.globalMaxTurns !== null && totalTurns >= budget.globalMaxTurns) {
577
- return "global-turns";
578
- }
579
- const totalTokens = budget.tokenUsageBaseline.totalTokens + currentTokenUsage.totalTokens;
580
- if (budget.maxTokens !== null && totalTokens >= budget.maxTokens) {
581
- return "tokens";
582
- }
583
- if (budget.sessionTimeoutMs !== null && budget.sessionStartedAt !== null && now.getTime() - new Date(budget.sessionStartedAt).getTime() >= budget.sessionTimeoutMs) {
584
- return "session-timeout";
585
- }
586
- return null;
587
- }
588
- function parsePositiveInteger(value) {
589
- if (!value) {
590
- return null;
591
- }
592
- const parsed = Number(value);
593
- if (!Number.isFinite(parsed) || parsed <= 0) {
594
- return null;
595
- }
596
- return Math.floor(parsed);
597
- }
598
- function parseNonNegativeInteger(value) {
599
- if (!value) {
600
- return 0;
601
- }
602
- const parsed = Number(value);
603
- if (!Number.isFinite(parsed) || parsed < 0) {
604
- return 0;
605
- }
606
- return Math.floor(parsed);
607
- }
608
- function normalizeTimestamp(value) {
609
- if (!value) {
610
- return null;
611
- }
612
- const parsed = Date.parse(value);
613
- return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
614
- }
615
-
616
- // ../worker/dist/thread-resume.js
142
+ // ../worker/src/thread-resume.ts
617
143
  var DEFAULT_CONTINUATION_GUIDANCE = "Continue working on the issue. Review your progress and complete any remaining tasks.";
618
- function parseNonNegativeInteger2(value) {
144
+ function parseNonNegativeInteger(value) {
619
145
  const parsed = typeof value === "number" ? value : Number.parseInt(value ?? "", 10);
620
146
  if (!Number.isFinite(parsed) || parsed <= 0) {
621
147
  return 0;
622
148
  }
623
149
  return Math.floor(parsed);
624
150
  }
625
- function resolveRemainingTurns(maxTurns, cumulativeTurnCount) {
626
- return Math.max(0, parseNonNegativeInteger2(maxTurns) - parseNonNegativeInteger2(cumulativeTurnCount));
627
- }
628
- function buildInitialTurnInput({ renderedPrompt, mode, lastTurnSummary, cumulativeTurnCount = 0, continuationGuidance }) {
629
- if (mode === "fresh") {
630
- return renderedPrompt;
631
- }
632
- const renderedContinuationGuidance = buildContinuationTurnInput({
633
- continuationGuidance,
634
- lastTurnSummary,
635
- cumulativeTurnCount
636
- });
637
- const normalizedSummary = normalizeContinuationVariable(lastTurnSummary) ?? "No previous turn summary was captured.";
638
- const normalizedCumulativeTurnCount = Math.max(0, parseNonNegativeInteger2(cumulativeTurnCount));
639
- if (mode === "resume") {
640
- return [
641
- "Resume work on this issue using the existing thread context.",
642
- `Previous worker turns completed: ${normalizedCumulativeTurnCount}.`,
643
- `Previous session summary: ${normalizedSummary}`,
644
- renderedContinuationGuidance
645
- ].join("\n");
646
- }
647
- return [
648
- "Resume work on this issue from a previous worker session.",
649
- "",
650
- "Original issue instructions:",
651
- renderedPrompt,
652
- "",
653
- "Previous session summary:",
654
- normalizedSummary,
655
- "",
656
- renderedContinuationGuidance
657
- ].join("\n");
658
- }
659
- function buildContinuationTurnInput({ continuationGuidance, lastTurnSummary, cumulativeTurnCount = 0 }) {
151
+ function buildContinuationTurnInput({
152
+ continuationGuidance,
153
+ lastTurnSummary,
154
+ cumulativeTurnCount = 0
155
+ }) {
660
156
  const template = continuationGuidance?.trim() || DEFAULT_CONTINUATION_GUIDANCE;
661
157
  return renderContinuationGuidance(template, {
662
158
  lastTurnSummary: normalizeContinuationVariable(lastTurnSummary) ?? "No previous turn summary was captured.",
663
- cumulativeTurnCount: String(Math.max(0, parseNonNegativeInteger2(cumulativeTurnCount)))
159
+ cumulativeTurnCount: String(
160
+ Math.max(0, parseNonNegativeInteger(cumulativeTurnCount))
161
+ )
664
162
  });
665
163
  }
666
164
  function normalizeContinuationVariable(value) {
@@ -669,7 +167,9 @@ function normalizeContinuationVariable(value) {
669
167
  }
670
168
  function renderContinuationGuidance(template, variables) {
671
169
  if (template.includes("{%") || template.includes("%}")) {
672
- throw new Error("template_parse_error: continuation guidance does not support Liquid tags.");
170
+ throw new Error(
171
+ "template_parse_error: continuation guidance does not support Liquid tags."
172
+ );
673
173
  }
674
174
  let rendered = "";
675
175
  let lastIndex = 0;
@@ -680,7 +180,9 @@ function renderContinuationGuidance(template, variables) {
680
180
  const index = match.index ?? 0;
681
181
  rendered += template.slice(lastIndex, index);
682
182
  if (!(expression in variables)) {
683
- throw new Error(`template_render_error: unsupported continuation guidance variable '${expression}'.`);
183
+ throw new Error(
184
+ `template_render_error: unsupported continuation guidance variable '${expression}'.`
185
+ );
684
186
  }
685
187
  rendered += variables[expression] ?? "";
686
188
  lastIndex = index + matchedText.length;
@@ -688,26 +190,57 @@ function renderContinuationGuidance(template, variables) {
688
190
  rendered += template.slice(lastIndex);
689
191
  const strayLiquidExpression = rendered.match(/\{\{[^}]*\}\}/);
690
192
  if (strayLiquidExpression) {
691
- throw new Error(`template_parse_error: invalid continuation guidance expression '${strayLiquidExpression[0]}'.`);
193
+ throw new Error(
194
+ `template_parse_error: invalid continuation guidance expression '${strayLiquidExpression[0]}'.`
195
+ );
692
196
  }
693
197
  return rendered;
694
198
  }
695
199
 
696
- // ../worker/dist/token-usage.js
697
- import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
698
- import { dirname as dirname2, join as join2 } from "path";
200
+ // ../worker/src/turn-limits.ts
201
+ var DEFAULT_SESSION_MAX_TURNS = 20;
202
+ function resolveMaxTurns(value) {
203
+ const parsed = typeof value === "number" ? value : Number(value);
204
+ if (!Number.isFinite(parsed)) {
205
+ return {
206
+ maxTurns: DEFAULT_SESSION_MAX_TURNS,
207
+ exhaustedBeforeStart: false
208
+ };
209
+ }
210
+ const normalized = Math.trunc(parsed);
211
+ if (normalized <= 0) {
212
+ return {
213
+ maxTurns: 0,
214
+ exhaustedBeforeStart: true
215
+ };
216
+ }
217
+ return {
218
+ maxTurns: normalized,
219
+ exhaustedBeforeStart: false
220
+ };
221
+ }
222
+
223
+ // ../worker/src/token-usage.ts
224
+ import { mkdir, writeFile } from "fs/promises";
225
+ import { dirname, join } from "path";
699
226
  async function persistTokenUsageArtifact(env, tokenUsage) {
700
227
  const artifactPath = resolveTokenUsageArtifactPath(env);
701
228
  if (!artifactPath) {
702
229
  return;
703
230
  }
704
231
  try {
705
- await mkdir2(dirname2(artifactPath), { recursive: true });
706
- await writeFile3(artifactPath, JSON.stringify(tokenUsage, null, 2) + "\n", "utf8");
232
+ await mkdir(dirname(artifactPath), { recursive: true });
233
+ await writeFile(
234
+ artifactPath,
235
+ JSON.stringify(tokenUsage, null, 2) + "\n",
236
+ "utf8"
237
+ );
707
238
  } catch (error) {
708
239
  const message = error instanceof Error ? error.message : String(error);
709
- process.stderr.write(`[worker] failed to persist token usage artifact: ${message}
710
- `);
240
+ process.stderr.write(
241
+ `[worker] failed to persist token usage artifact: ${message}
242
+ `
243
+ );
711
244
  }
712
245
  }
713
246
  function resolveTokenUsageArtifactPath(env) {
@@ -715,12 +248,11 @@ function resolveTokenUsageArtifactPath(env) {
715
248
  if (!workspaceRuntimeDir) {
716
249
  return null;
717
250
  }
718
- return join2(workspaceRuntimeDir, "token-usage.json");
251
+ return join(workspaceRuntimeDir, "token-usage.json");
719
252
  }
720
253
 
721
- // ../worker/dist/index.js
254
+ // ../worker/src/index.ts
722
255
  var launcherEnv = loadLauncherEnvironment(process.env);
723
- var sessionBudgetState = resolveSessionBudgetState(launcherEnv);
724
256
  var runtimeState = {
725
257
  status: launcherEnv.SYMPHONY_RUN_ID ? "starting" : "idle",
726
258
  executionPhase: null,
@@ -741,9 +273,9 @@ var runtimeState = {
741
273
  lastError: null
742
274
  } : null,
743
275
  tokenUsage: {
744
- inputTokens: sessionBudgetState.tokenUsageBaseline.inputTokens,
745
- outputTokens: sessionBudgetState.tokenUsageBaseline.outputTokens,
746
- totalTokens: sessionBudgetState.tokenUsageBaseline.totalTokens
276
+ inputTokens: 0,
277
+ outputTokens: 0,
278
+ totalTokens: 0
747
279
  },
748
280
  lastEventAt: null,
749
281
  rateLimits: null,
@@ -755,10 +287,16 @@ var runtimeState = {
755
287
  exitClassification: null
756
288
  }
757
289
  };
758
- console.log(JSON.stringify({
759
- package: "@gh-symphony/worker",
760
- runtime: "self-hosted-sample"
761
- }, null, 2));
290
+ console.log(
291
+ JSON.stringify(
292
+ {
293
+ package: "@gh-symphony/worker",
294
+ runtime: "self-hosted-sample"
295
+ },
296
+ null,
297
+ 2
298
+ )
299
+ );
762
300
  var childProcess = null;
763
301
  var shutdownPromise = null;
764
302
  var orchestratorChannelDrainPending = false;
@@ -793,7 +331,9 @@ function shutdown(signal) {
793
331
  stopOrchestratorHeartbeatTimer();
794
332
  emitOrchestratorHeartbeat();
795
333
  await persistSessionTokenUsageArtifact(launcherEnv);
796
- await waitForPendingOrchestratorChannelFlush(resolveTerminalOrchestratorChannelFlushTimeoutMs());
334
+ await waitForPendingOrchestratorChannelFlush(
335
+ resolveTerminalOrchestratorChannelFlushTimeoutMs()
336
+ );
797
337
  console.log(`Worker stopped on ${signal}`);
798
338
  process.exit(0);
799
339
  })();
@@ -826,13 +366,13 @@ function waitForPendingOrchestratorChannelFlush(timeoutMs = ORCHESTRATOR_CHANNEL
826
366
  if (!orchestratorChannelDrainPending && pendingOrchestratorChannelPayloads.length === 0) {
827
367
  return Promise.resolve();
828
368
  }
829
- return new Promise((resolve2) => {
369
+ return new Promise((resolve) => {
830
370
  let settled = false;
831
371
  let timeout = setTimeout(() => {
832
372
  settled = true;
833
373
  process.stderr.removeListener("drain", handleDrain);
834
374
  timeout = null;
835
- resolve2();
375
+ resolve();
836
376
  }, timeoutMs);
837
377
  const handleDrain = () => {
838
378
  if (orchestratorChannelDrainPending || pendingOrchestratorChannelPayloads.length > 0) {
@@ -847,14 +387,17 @@ function waitForPendingOrchestratorChannelFlush(timeoutMs = ORCHESTRATOR_CHANNEL
847
387
  timeout = null;
848
388
  }
849
389
  process.stderr.removeListener("drain", handleDrain);
850
- resolve2();
390
+ resolve();
851
391
  };
852
392
  process.stderr.on("drain", handleDrain);
853
393
  });
854
394
  }
855
395
  function resolveTerminalOrchestratorChannelFlushTimeoutMs() {
856
396
  const pendingPayloadCount = pendingOrchestratorChannelPayloads.length + (orchestratorChannelDrainPending ? 1 : 0);
857
- return Math.max(ORCHESTRATOR_CHANNEL_FLUSH_TIMEOUT_MS, pendingPayloadCount * ORCHESTRATOR_CHANNEL_FLUSH_TIMEOUT_MS);
397
+ return Math.max(
398
+ ORCHESTRATOR_CHANNEL_FLUSH_TIMEOUT_MS,
399
+ pendingPayloadCount * ORCHESTRATOR_CHANNEL_FLUSH_TIMEOUT_MS
400
+ );
858
401
  }
859
402
  function writeOrQueueOrchestratorChannelPayload(serializedPayload) {
860
403
  if (orchestratorChannelDrainPending) {
@@ -932,13 +475,22 @@ function cloneTokenUsageSnapshot() {
932
475
  }
933
476
  function resolveTurnTokenUsageDelta(baseline) {
934
477
  return {
935
- inputTokens: Math.max(0, runtimeState.tokenUsage.inputTokens - baseline.inputTokens),
936
- outputTokens: Math.max(0, runtimeState.tokenUsage.outputTokens - baseline.outputTokens),
937
- totalTokens: Math.max(0, runtimeState.tokenUsage.totalTokens - baseline.totalTokens)
478
+ inputTokens: Math.max(
479
+ 0,
480
+ runtimeState.tokenUsage.inputTokens - baseline.inputTokens
481
+ ),
482
+ outputTokens: Math.max(
483
+ 0,
484
+ runtimeState.tokenUsage.outputTokens - baseline.outputTokens
485
+ ),
486
+ totalTokens: Math.max(
487
+ 0,
488
+ runtimeState.tokenUsage.totalTokens - baseline.totalTokens
489
+ )
938
490
  };
939
491
  }
940
492
  function resolveSessionTokenUsageDelta() {
941
- return resolveTurnTokenUsageDelta(sessionBudgetState.tokenUsageBaseline);
493
+ return cloneTokenUsageSnapshot();
942
494
  }
943
495
  async function persistSessionTokenUsageArtifact(env) {
944
496
  await persistTokenUsageArtifact(env, resolveSessionTokenUsageDelta());
@@ -971,7 +523,10 @@ function emitTurnCompletedEvent(turn) {
971
523
  issueId,
972
524
  startedAt: turn.startedAt,
973
525
  completedAt,
974
- durationMs: Math.max(0, new Date(completedAt).getTime() - new Date(turn.startedAt).getTime()),
526
+ durationMs: Math.max(
527
+ 0,
528
+ new Date(completedAt).getTime() - new Date(turn.startedAt).getTime()
529
+ ),
975
530
  threadId: turn.threadId,
976
531
  turnId: turn.turnId,
977
532
  turnCount: turn.turnCount,
@@ -992,7 +547,10 @@ function emitTurnFailedEvent(turn, error) {
992
547
  issueId,
993
548
  startedAt: turn.startedAt,
994
549
  failedAt,
995
- durationMs: Math.max(0, new Date(failedAt).getTime() - new Date(turn.startedAt).getTime()),
550
+ durationMs: Math.max(
551
+ 0,
552
+ new Date(failedAt).getTime() - new Date(turn.startedAt).getTime()
553
+ ),
996
554
  threadId: turn.threadId,
997
555
  turnId: turn.turnId,
998
556
  turnCount: turn.turnCount,
@@ -1005,16 +563,42 @@ function emitTurnFailedEvent(turn, error) {
1005
563
  }
1006
564
  async function startAssignedRun() {
1007
565
  try {
1008
- const workflowPath = launcherEnv.SYMPHONY_WORKFLOW_PATH || join3(launcherEnv.WORKING_DIRECTORY, "WORKFLOW.md");
566
+ const workflowPath = launcherEnv.SYMPHONY_WORKFLOW_PATH || join2(launcherEnv.WORKING_DIRECTORY, "WORKFLOW.md");
1009
567
  runtimeState.runPhase = "building_prompt";
1010
- const workflow = parseWorkflowMarkdown(await readFile2(workflowPath, "utf8"), launcherEnv);
568
+ const workflow = parseWorkflowMarkdown(
569
+ await readFile(workflowPath, "utf8"),
570
+ launcherEnv
571
+ );
572
+ if (isClaudeRuntimeCommand(workflow.codex.command)) {
573
+ const hasGitHubGraphqlToken = typeof launcherEnv.GITHUB_GRAPHQL_TOKEN === "string" && launcherEnv.GITHUB_GRAPHQL_TOKEN.trim().length > 0;
574
+ const preflight = await runClaudePreflight({
575
+ cwd: launcherEnv.WORKING_DIRECTORY,
576
+ env: launcherEnv,
577
+ command: resolveClaudeCommandBinary(workflow.codex.command) ?? void 0,
578
+ includeGhAuth: !hasGitHubGraphqlToken
579
+ });
580
+ process.stderr.write(
581
+ `[worker] ${formatClaudePreflightText(preflight)}
582
+ `
583
+ );
584
+ if (!preflight.ok) {
585
+ await exitWorkerStartupFailure(
586
+ "Claude runtime preflight failed before agent launch."
587
+ );
588
+ return;
589
+ }
590
+ await exitWorkerStartupFailure(
591
+ "Claude runtime worker launch is not yet implemented in this branch."
592
+ );
593
+ return;
594
+ }
1011
595
  runtimeState.executionPhase = resolveInitialExecutionPhase({
1012
596
  issueState: runtimeState.run?.state,
1013
597
  blockerCheckStates: workflow.lifecycle.blockerCheckStates,
1014
598
  activeStates: workflow.lifecycle.activeStates
1015
599
  });
1016
600
  const config = resolveLocalRuntimeLaunchConfig(launcherEnv);
1017
- config.agentCommand = workflow.codex.command;
601
+ config.agentCommand = resolveWorkflowRuntimeCommand(workflow);
1018
602
  runtimeState.runPhase = "launching_agent";
1019
603
  const plan = await prepareCodexRuntimePlan(config);
1020
604
  childProcess = launchCodexAppServer(plan);
@@ -1026,24 +610,27 @@ async function startAssignedRun() {
1026
610
  void runCodexClientProtocol(childProcess, plan, launcherEnv, {
1027
611
  continuationGuidance: workflow.continuationGuidance
1028
612
  });
1029
- childProcess.once("exit", (code, signal) => {
1030
- const currentRunPhase = runtimeState.runPhase;
1031
- const nextRunPhase = resolveExitRunPhase(currentRunPhase, {
1032
- code,
1033
- signal
1034
- });
1035
- const preservesTerminalPhase = currentRunPhase != null && nextRunPhase === currentRunPhase;
1036
- if (!preservesTerminalPhase) {
1037
- runtimeState.status = code === 0 && !signal ? "completed" : "failed";
1038
- }
1039
- runtimeState.runPhase = nextRunPhase;
1040
- if (runtimeState.run) {
613
+ childProcess.once(
614
+ "exit",
615
+ (code, signal) => {
616
+ const currentRunPhase = runtimeState.runPhase;
617
+ const nextRunPhase = resolveExitRunPhase(currentRunPhase, {
618
+ code,
619
+ signal
620
+ });
621
+ const preservesTerminalPhase = currentRunPhase != null && nextRunPhase === currentRunPhase;
1041
622
  if (!preservesTerminalPhase) {
1042
- runtimeState.run.lastError = code === 0 && !signal ? null : `codex app-server exited with ${signal ?? code ?? "unknown"}`;
623
+ runtimeState.status = code === 0 && !signal ? "completed" : "failed";
1043
624
  }
625
+ runtimeState.runPhase = nextRunPhase;
626
+ if (runtimeState.run) {
627
+ if (!preservesTerminalPhase) {
628
+ runtimeState.run.lastError = code === 0 && !signal ? null : `codex app-server exited with ${signal ?? code ?? "unknown"}`;
629
+ }
630
+ }
631
+ void persistSessionTokenUsageArtifact(launcherEnv);
1044
632
  }
1045
- void persistSessionTokenUsageArtifact(launcherEnv);
1046
- });
633
+ );
1047
634
  childProcess.once("error", (error) => {
1048
635
  runtimeState.status = "failed";
1049
636
  runtimeState.runPhase = "failed";
@@ -1061,27 +648,45 @@ async function startAssignedRun() {
1061
648
  await persistSessionTokenUsageArtifact(launcherEnv);
1062
649
  }
1063
650
  }
651
+ async function exitWorkerStartupFailure(message) {
652
+ runtimeState.status = "failed";
653
+ runtimeState.runPhase = "failed";
654
+ if (runtimeState.run) {
655
+ runtimeState.run.lastError = message;
656
+ }
657
+ process.stderr.write(`[worker] ${message}
658
+ `);
659
+ stopOrchestratorHeartbeatTimer();
660
+ emitOrchestratorHeartbeat();
661
+ await persistSessionTokenUsageArtifact(launcherEnv);
662
+ await waitForPendingOrchestratorChannelFlush(
663
+ resolveTerminalOrchestratorChannelFlushTimeoutMs()
664
+ );
665
+ process.exit(1);
666
+ }
1064
667
  async function runCodexClientProtocol(child, plan, env, options) {
1065
668
  const renderedPrompt = env.SYMPHONY_RENDERED_PROMPT;
1066
669
  if (!renderedPrompt) {
1067
- process.stderr.write("[worker] SYMPHONY_RENDERED_PROMPT not set; skipping codex client protocol\n");
670
+ process.stderr.write(
671
+ "[worker] SYMPHONY_RENDERED_PROMPT not set; skipping codex client protocol\n"
672
+ );
1068
673
  return;
1069
674
  }
1070
675
  if (!child.stdin || !child.stdout) {
1071
- process.stderr.write("[worker] codex process has no stdio pipes; cannot run client protocol\n");
676
+ process.stderr.write(
677
+ "[worker] codex process has no stdio pipes; cannot run client protocol\n"
678
+ );
1072
679
  return;
1073
680
  }
1074
- const maxTurns = Number(env.SYMPHONY_MAX_TURNS) || 20;
1075
- const cumulativeTurnCount = parseNonNegativeInteger2(env.SYMPHONY_CUMULATIVE_TURN_COUNT);
1076
- const remainingTurns = resolveRemainingTurns(maxTurns, cumulativeTurnCount);
681
+ const { maxTurns, exhaustedBeforeStart } = resolveMaxTurns(
682
+ env.SYMPHONY_MAX_TURNS
683
+ );
1077
684
  const readTimeoutMs = Number(env.SYMPHONY_READ_TIMEOUT_MS) || 5e3;
1078
685
  const turnTimeoutMs = Number(env.SYMPHONY_TURN_TIMEOUT_MS) || 36e5;
1079
686
  const maxNonProductiveTurns = resolveMaxNonProductiveTurns(env);
1080
687
  const issueIdentifier = env.SYMPHONY_ISSUE_IDENTIFIER ?? "";
1081
- const lastTurnSummary = env.SYMPHONY_LAST_TURN_SUMMARY ?? null;
1082
688
  const continuationGuidance = env.SYMPHONY_CONTINUATION_GUIDANCE ?? options.continuationGuidance;
1083
689
  const { approvalPolicy, threadSandbox, turnSandboxPolicy } = resolveCodexPolicySettings(env);
1084
- const budgetState = resolveSessionBudgetState(env);
1085
690
  let previousTurnProgressSnapshot = {
1086
691
  ...captureTurnWorkspaceSnapshot(plan.cwd),
1087
692
  lastError: runtimeState.run?.lastError ?? null
@@ -1090,10 +695,11 @@ async function runCodexClientProtocol(child, plan, env, options) {
1090
695
  let lineBuffer = "";
1091
696
  let deltaBuffer = null;
1092
697
  function flushDeltaBuffer() {
1093
- if (!deltaBuffer)
1094
- return;
1095
- process.stderr.write(`[worker] codex \u2192 agent_message [accumulated] ${JSON.stringify({ text: deltaBuffer.text }).slice(0, 500)}
1096
- `);
698
+ if (!deltaBuffer) return;
699
+ process.stderr.write(
700
+ `[worker] codex \u2192 agent_message [accumulated] ${JSON.stringify({ text: deltaBuffer.text }).slice(0, 500)}
701
+ `
702
+ );
1097
703
  deltaBuffer = null;
1098
704
  }
1099
705
  const pendingRequests = /* @__PURE__ */ new Map();
@@ -1101,16 +707,8 @@ async function runCodexClientProtocol(child, plan, env, options) {
1101
707
  let userInputRequired = false;
1102
708
  let turnTerminalFailurePhase = null;
1103
709
  let activeTurnTelemetry = null;
1104
- let budgetExceededReason = null;
1105
710
  let consecutiveNonProductiveTurns = 0;
1106
711
  let convergenceDetected = false;
1107
- function checkSessionBudgets(currentSessionTurnCount) {
1108
- return resolveBudgetExceededReason(budgetState, currentSessionTurnCount, {
1109
- inputTokens: runtimeState.tokenUsage.inputTokens - budgetState.tokenUsageBaseline.inputTokens,
1110
- outputTokens: runtimeState.tokenUsage.outputTokens - budgetState.tokenUsageBaseline.outputTokens,
1111
- totalTokens: runtimeState.tokenUsage.totalTokens - budgetState.tokenUsageBaseline.totalTokens
1112
- }, /* @__PURE__ */ new Date());
1113
- }
1114
712
  function resolvePendingTurnCompletion() {
1115
713
  if (turnCompletedResolve) {
1116
714
  turnCompletedResolve();
@@ -1118,7 +716,8 @@ async function runCodexClientProtocol(child, plan, env, options) {
1118
716
  }
1119
717
  }
1120
718
  function describeTurnTerminalEvent(event, params) {
1121
- const fallback = event === "turn/failed" ? "turn_failed: codex reported turn failure" : "turn_cancelled: codex reported turn cancellation";
719
+ const errorPrefix = event === "agent.turnFailed" ? "turn_failed" : "turn_cancelled";
720
+ const fallback = event === "agent.turnFailed" ? "turn_failed: codex reported turn failure" : "turn_cancelled: codex reported turn cancellation";
1122
721
  if (!params || typeof params !== "object") {
1123
722
  return fallback;
1124
723
  }
@@ -1127,18 +726,18 @@ async function runCodexClientProtocol(child, plan, env, options) {
1127
726
  for (const key of directReasonKeys) {
1128
727
  const value = record[key];
1129
728
  if (typeof value === "string" && value.trim()) {
1130
- return `${event.replace("/", "_")}: ${value.trim()}`;
729
+ return `${errorPrefix}: ${value.trim()}`;
1131
730
  }
1132
731
  if (value && typeof value === "object" && typeof value.message === "string") {
1133
732
  const nested = value;
1134
733
  const nestedMessage = String(nested.message).trim();
1135
734
  if (nestedMessage) {
1136
- return `${event.replace("/", "_")}: ${nestedMessage}`;
735
+ return `${errorPrefix}: ${nestedMessage}`;
1137
736
  }
1138
737
  }
1139
738
  }
1140
739
  const serialized = JSON.stringify(params).slice(0, 300);
1141
- return serialized && serialized !== "{}" ? `${event.replace("/", "_")}: ${serialized}` : fallback;
740
+ return serialized && serialized !== "{}" ? `${errorPrefix}: ${serialized}` : fallback;
1142
741
  }
1143
742
  function markTurnTerminalFailure(runPhase, lastError) {
1144
743
  runtimeState.status = "failed";
@@ -1158,36 +757,45 @@ async function runCodexClientProtocol(child, plan, env, options) {
1158
757
  child.stdin?.write(line);
1159
758
  }
1160
759
  function sendRequest(id, method, params) {
1161
- return new Promise((resolve2, reject) => {
1162
- pendingRequests.set(id, { resolve: resolve2, reject });
760
+ return new Promise((resolve, reject) => {
761
+ pendingRequests.set(id, { resolve, reject });
1163
762
  sendMessage({ jsonrpc: "2.0", id, method, params });
1164
763
  });
1165
764
  }
1166
765
  function sendRequestWithTimeout(id, method, params) {
1167
- return new Promise((resolve2, reject) => {
766
+ return new Promise((resolve, reject) => {
1168
767
  const timer = setTimeout(() => {
1169
768
  pendingRequests.delete(id);
1170
- reject(new Error(`response_timeout: ${method} timed out after ${readTimeoutMs}ms`));
769
+ reject(
770
+ new Error(
771
+ `response_timeout: ${method} timed out after ${readTimeoutMs}ms`
772
+ )
773
+ );
1171
774
  }, readTimeoutMs);
1172
- sendRequest(id, method, params).then((result) => {
1173
- clearTimeout(timer);
1174
- resolve2(result);
1175
- }, (error) => {
1176
- clearTimeout(timer);
1177
- reject(error);
1178
- });
775
+ sendRequest(id, method, params).then(
776
+ (result) => {
777
+ clearTimeout(timer);
778
+ resolve(result);
779
+ },
780
+ (error) => {
781
+ clearTimeout(timer);
782
+ reject(error);
783
+ }
784
+ );
1179
785
  });
1180
786
  }
1181
787
  function waitForTurnCompletion() {
1182
- return new Promise((resolve2) => {
1183
- turnCompletedResolve = resolve2;
788
+ return new Promise((resolve) => {
789
+ turnCompletedResolve = resolve;
1184
790
  });
1185
791
  }
1186
792
  function waitForTurnWithTimeout() {
1187
- return new Promise((resolve2, reject) => {
793
+ return new Promise((resolve, reject) => {
1188
794
  const timer = setTimeout(() => {
1189
- process.stderr.write(`[worker] turn_timeout: turn exceeded ${turnTimeoutMs}ms \u2014 killing codex process
1190
- `);
795
+ process.stderr.write(
796
+ `[worker] turn_timeout: turn exceeded ${turnTimeoutMs}ms \u2014 killing codex process
797
+ `
798
+ );
1191
799
  if (child.pid) {
1192
800
  try {
1193
801
  process.kill(child.pid, "SIGTERM");
@@ -1196,20 +804,27 @@ async function runCodexClientProtocol(child, plan, env, options) {
1196
804
  }
1197
805
  reject(new Error("turn_timeout: turn exceeded time limit"));
1198
806
  }, turnTimeoutMs);
1199
- waitForTurnCompletion().then(() => {
1200
- clearTimeout(timer);
1201
- resolve2();
1202
- }, (error) => {
1203
- clearTimeout(timer);
1204
- reject(error);
1205
- });
807
+ waitForTurnCompletion().then(
808
+ () => {
809
+ clearTimeout(timer);
810
+ resolve();
811
+ },
812
+ (error) => {
813
+ clearTimeout(timer);
814
+ reject(error);
815
+ }
816
+ );
1206
817
  });
1207
818
  }
1208
819
  async function dispatchDynamicToolCall(callId, toolName, threadId, turnId, args) {
1209
- const toolDef = plan.tools.find((t) => t.name === toolName);
820
+ const toolDef = plan.tools.find(
821
+ (t) => t.name === toolName
822
+ );
1210
823
  if (!toolDef) {
1211
- process.stderr.write(`[worker] unknown dynamic tool: ${toolName}; sending error response
1212
- `);
824
+ process.stderr.write(
825
+ `[worker] unknown dynamic tool: ${toolName}; sending error response
826
+ `
827
+ );
1213
828
  sendMessage({
1214
829
  jsonrpc: "2.0",
1215
830
  method: "dynamic_tool_call_response",
@@ -1229,8 +844,10 @@ async function runCodexClientProtocol(child, plan, env, options) {
1229
844
  return;
1230
845
  }
1231
846
  const inputJson = JSON.stringify(args ?? {});
1232
- process.stderr.write(`[worker] executing dynamic tool "${toolName}" (callId=${callId})
1233
- `);
847
+ process.stderr.write(
848
+ `[worker] executing dynamic tool "${toolName}" (callId=${callId})
849
+ `
850
+ );
1234
851
  try {
1235
852
  const output = await runToolProcess(toolDef, inputJson);
1236
853
  sendMessage({
@@ -1261,138 +878,185 @@ async function runCodexClientProtocol(child, plan, env, options) {
1261
878
  });
1262
879
  }
1263
880
  }
1264
- function handleServerMessage(msg) {
1265
- if ("id" in msg && msg.id != null && ("result" in msg || "error" in msg)) {
1266
- const id = String(msg.id);
1267
- const pending = pendingRequests.get(id);
1268
- if (pending) {
1269
- pendingRequests.delete(id);
1270
- if ("error" in msg) {
1271
- pending.reject(new Error(JSON.stringify(msg.error)));
1272
- } else {
1273
- pending.resolve(msg.result);
1274
- }
1275
- }
881
+ function emitObservedAgentEvent(event) {
882
+ if (event.payload.suppressUpdate) {
1276
883
  return;
1277
884
  }
1278
- runtimeState.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
1279
- const orchestratorEventName = typeof msg.method === "string" ? msg.method : void 0;
1280
- if (msg.method === "dynamic_tool_call_request" && msg.params != null) {
1281
- const params = msg.params;
1282
- void dispatchDynamicToolCall(params.callId, params.tool, params.threadId, params.turnId, params.arguments);
1283
- emitOrchestratorChannelEvent(orchestratorEventName);
1284
- return;
885
+ emitOrchestratorChannelEvent(getCodexObservabilityEventName(event));
886
+ }
887
+ function handleInputRequired(reason, event) {
888
+ process.stderr.write(
889
+ "[worker] user_input_required detected \u2014 terminating agent process\n"
890
+ );
891
+ userInputRequired = true;
892
+ runtimeState.status = "failed";
893
+ if (runtimeState.run) {
894
+ runtimeState.run.lastError = reason;
1285
895
  }
1286
- if (msg.method === "item/tool/requestUserInput") {
1287
- process.stderr.write("[worker] user_input_required detected \u2014 terminating codex process\n");
1288
- userInputRequired = true;
1289
- runtimeState.status = "failed";
1290
- if (runtimeState.run) {
1291
- runtimeState.run.lastError = "turn_input_required: agent requires user input";
1292
- }
1293
- if (child.pid) {
1294
- try {
1295
- process.kill(child.pid, "SIGTERM");
1296
- } catch {
1297
- }
1298
- }
1299
- if (activeTurnTelemetry) {
1300
- emitTurnFailedEvent(activeTurnTelemetry, runtimeState.run?.lastError ?? null);
1301
- activeTurnTelemetry = null;
896
+ if (child.pid) {
897
+ try {
898
+ process.kill(child.pid, "SIGTERM");
899
+ } catch {
1302
900
  }
1303
- resolvePendingTurnCompletion();
1304
- emitOrchestratorChannelEvent(orchestratorEventName);
1305
- return;
1306
901
  }
1307
- if (msg.method === "turn/completed") {
1308
- flushDeltaBuffer();
1309
- const turnParams = msg.params ?? {};
1310
- const turnUsage = extractAbsoluteTokenUsage(turnParams.usage);
1311
- if (turnUsage) {
1312
- applyTokenUsageUpdate("turn/completed", turnUsage);
902
+ if (activeTurnTelemetry) {
903
+ emitTurnFailedEvent(
904
+ activeTurnTelemetry,
905
+ runtimeState.run?.lastError ?? null
906
+ );
907
+ activeTurnTelemetry = null;
908
+ }
909
+ resolvePendingTurnCompletion();
910
+ emitObservedAgentEvent(event);
911
+ }
912
+ function handleAgentEvent(event) {
913
+ switch (event.name) {
914
+ case "agent.turnStarted":
915
+ emitObservedAgentEvent(event);
916
+ return true;
917
+ case "agent.toolCallRequested":
918
+ if (!event.payload.threadId || !event.payload.turnId) {
919
+ process.stderr.write(
920
+ `[worker] dynamic tool call ${event.payload.callId} is missing threadId/turnId; cannot send response
921
+ `
922
+ );
923
+ emitObservedAgentEvent(event);
924
+ return true;
925
+ }
926
+ void dispatchDynamicToolCall(
927
+ event.payload.callId,
928
+ event.payload.toolName,
929
+ event.payload.threadId,
930
+ event.payload.turnId,
931
+ event.payload.arguments
932
+ );
933
+ emitObservedAgentEvent(event);
934
+ return true;
935
+ case "agent.inputRequired":
936
+ handleInputRequired(event.payload.reason, event);
937
+ return true;
938
+ case "agent.tokenUsageUpdated": {
939
+ const tokenUsage = extractAbsoluteTokenUsage(event.payload.params);
940
+ if (tokenUsage) {
941
+ applyTokenUsageUpdate(
942
+ getCodexObservabilityEventName(event) ?? event.name,
943
+ tokenUsage
944
+ );
945
+ }
946
+ emitObservedAgentEvent(event);
947
+ return true;
1313
948
  }
1314
- const rateLimits2 = extractRateLimitPayload(turnParams);
1315
- if (rateLimits2) {
1316
- applyRateLimitUpdate("turn/completed", rateLimits2);
949
+ case "agent.rateLimit": {
950
+ const rateLimits = extractRateLimitPayload(event.payload.params);
951
+ if (rateLimits) {
952
+ applyRateLimitUpdate(
953
+ getCodexObservabilityEventName(event) ?? event.name,
954
+ rateLimits
955
+ );
956
+ }
957
+ emitObservedAgentEvent(event);
958
+ return true;
1317
959
  }
1318
- if (turnParams.inputRequired === true) {
1319
- process.stderr.write("[worker] user_input_required detected \u2014 terminating codex process\n");
1320
- userInputRequired = true;
1321
- runtimeState.status = "failed";
1322
- if (runtimeState.run) {
1323
- runtimeState.run.lastError = "turn_input_required: agent requires user input";
960
+ case "agent.messageDelta": {
961
+ const { delta, itemId } = event.payload;
962
+ if (deltaBuffer?.itemId !== itemId) {
963
+ flushDeltaBuffer();
964
+ deltaBuffer = { itemId, text: delta };
965
+ } else {
966
+ deltaBuffer.text += delta;
1324
967
  }
1325
- if (child.pid) {
1326
- try {
1327
- process.kill(child.pid, "SIGTERM");
1328
- } catch {
1329
- }
968
+ emitObservedAgentEvent(event);
969
+ return true;
970
+ }
971
+ case "agent.turnCompleted":
972
+ flushDeltaBuffer();
973
+ if (event.payload.inputRequired) {
974
+ handleInputRequired(DEFAULT_AGENT_INPUT_REQUIRED_REASON, event);
975
+ return true;
1330
976
  }
977
+ emitObservedAgentEvent(event);
1331
978
  if (activeTurnTelemetry) {
1332
- emitTurnFailedEvent(activeTurnTelemetry, runtimeState.run?.lastError ?? null);
979
+ emitTurnCompletedEvent(activeTurnTelemetry);
1333
980
  activeTurnTelemetry = null;
1334
981
  }
982
+ process.stderr.write("[worker] agent turn completed\n");
1335
983
  resolvePendingTurnCompletion();
1336
- emitOrchestratorChannelEvent(orchestratorEventName);
1337
- return;
984
+ return true;
985
+ case "agent.turnFailed": {
986
+ flushDeltaBuffer();
987
+ const lastError = describeTurnTerminalEvent(
988
+ "agent.turnFailed",
989
+ event.payload.params
990
+ );
991
+ process.stderr.write(
992
+ `[worker] agent turn failed ${JSON.stringify(event.payload.params).slice(0, 300)}
993
+ `
994
+ );
995
+ markTurnTerminalFailure("failed", lastError);
996
+ emitObservedAgentEvent(event);
997
+ return true;
1338
998
  }
1339
- emitOrchestratorChannelEvent(orchestratorEventName);
1340
- if (activeTurnTelemetry) {
1341
- emitTurnCompletedEvent(activeTurnTelemetry);
1342
- activeTurnTelemetry = null;
999
+ case "agent.turnCancelled": {
1000
+ flushDeltaBuffer();
1001
+ const lastError = describeTurnTerminalEvent(
1002
+ "agent.turnCancelled",
1003
+ event.payload.params
1004
+ );
1005
+ process.stderr.write(
1006
+ `[worker] agent turn cancelled ${JSON.stringify(event.payload.params).slice(0, 300)}
1007
+ `
1008
+ );
1009
+ markTurnTerminalFailure("canceled_by_reconciliation", lastError);
1010
+ emitObservedAgentEvent(event);
1011
+ return true;
1343
1012
  }
1344
- process.stderr.write("[worker] codex turn/completed\n");
1345
- resolvePendingTurnCompletion();
1346
- return;
1013
+ case "agent.error":
1014
+ flushDeltaBuffer();
1015
+ process.stderr.write(
1016
+ `[worker] runtime error ${JSON.stringify(event.payload.params).slice(0, 300)}
1017
+ `
1018
+ );
1019
+ markTurnTerminalFailure("failed", event.payload.error);
1020
+ emitObservedAgentEvent(event);
1021
+ return true;
1022
+ default:
1023
+ return false;
1347
1024
  }
1348
- if (msg.method === "turn/failed") {
1349
- flushDeltaBuffer();
1350
- const lastError = describeTurnTerminalEvent("turn/failed", msg.params ?? null);
1351
- process.stderr.write(`[worker] codex turn/failed ${JSON.stringify(msg.params ?? {}).slice(0, 300)}
1352
- `);
1353
- markTurnTerminalFailure("failed", lastError);
1354
- emitOrchestratorChannelEvent(orchestratorEventName);
1025
+ }
1026
+ function handleServerMessage(msg) {
1027
+ if ("id" in msg && msg.id != null && ("result" in msg || "error" in msg)) {
1028
+ const id = String(msg.id);
1029
+ const pending = pendingRequests.get(id);
1030
+ if (pending) {
1031
+ pendingRequests.delete(id);
1032
+ if ("error" in msg) {
1033
+ pending.reject(new Error(JSON.stringify(msg.error)));
1034
+ } else {
1035
+ pending.resolve(msg.result);
1036
+ }
1037
+ }
1355
1038
  return;
1356
1039
  }
1357
- if (msg.method === "turn/cancelled") {
1358
- flushDeltaBuffer();
1359
- const lastError = describeTurnTerminalEvent("turn/cancelled", msg.params ?? null);
1360
- process.stderr.write(`[worker] codex turn/cancelled ${JSON.stringify(msg.params ?? {}).slice(0, 300)}
1361
- `);
1362
- markTurnTerminalFailure("canceled_by_reconciliation", lastError);
1363
- emitOrchestratorChannelEvent(orchestratorEventName);
1364
- return;
1040
+ runtimeState.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
1041
+ const agentEvents = normalizeCodexRuntimeEvents(msg);
1042
+ let handledAgentEvent = false;
1043
+ for (const event of agentEvents) {
1044
+ handledAgentEvent = handleAgentEvent(event) || handledAgentEvent;
1365
1045
  }
1366
- if (msg.method === "thread/tokenUsage/updated" || msg.method === "total_token_usage" || msg.method === "codex/event/token_count") {
1367
- const tokenUsage = extractAbsoluteTokenUsage(msg.params);
1368
- if (tokenUsage) {
1369
- applyTokenUsageUpdate(msg.method, tokenUsage);
1370
- }
1371
- emitOrchestratorChannelEvent(orchestratorEventName);
1046
+ if (handledAgentEvent) {
1372
1047
  return;
1373
1048
  }
1374
1049
  const rateLimits = extractRateLimitPayload(msg.params);
1375
1050
  if (rateLimits && typeof msg.method === "string") {
1376
1051
  applyRateLimitUpdate(msg.method, rateLimits);
1377
1052
  }
1378
- if (typeof msg.method === "string" && (msg.method === "codex/event/agent_message_content_delta" || msg.method === "codex/event/agent_message_delta" || msg.method === "item/agentMessage/delta")) {
1379
- const params = msg.params ?? {};
1380
- const delta = typeof params.delta === "string" ? params.delta : "";
1381
- const itemId = typeof params.item_id === "string" ? params.item_id : "";
1382
- if (deltaBuffer?.itemId !== itemId) {
1383
- flushDeltaBuffer();
1384
- deltaBuffer = { itemId, text: delta };
1385
- } else {
1386
- deltaBuffer.text += delta;
1387
- }
1388
- emitOrchestratorChannelEvent(orchestratorEventName);
1389
- return;
1390
- }
1391
1053
  if (typeof msg.method === "string") {
1392
1054
  flushDeltaBuffer();
1393
- emitOrchestratorChannelEvent(orchestratorEventName);
1394
- process.stderr.write(`[worker] codex \u2192 ${msg.method} ${JSON.stringify(msg.params ?? {}).slice(0, 300)}
1395
- `);
1055
+ emitOrchestratorChannelEvent(msg.method);
1056
+ process.stderr.write(
1057
+ `[worker] codex \u2192 ${msg.method} ${JSON.stringify(msg.params ?? {}).slice(0, 300)}
1058
+ `
1059
+ );
1396
1060
  }
1397
1061
  }
1398
1062
  child.stdout.on("data", (chunk) => {
@@ -1401,8 +1065,7 @@ async function runCodexClientProtocol(child, plan, env, options) {
1401
1065
  lineBuffer = lines.pop() ?? "";
1402
1066
  for (const line of lines) {
1403
1067
  const trimmed = line.trim();
1404
- if (!trimmed)
1405
- continue;
1068
+ if (!trimmed) continue;
1406
1069
  try {
1407
1070
  const msg = JSON.parse(trimmed);
1408
1071
  handleServerMessage(msg);
@@ -1428,27 +1091,6 @@ async function runCodexClientProtocol(child, plan, env, options) {
1428
1091
  env: t.env
1429
1092
  };
1430
1093
  }
1431
- if (remainingTurns <= 0) {
1432
- process.stderr.write(`[worker] max_turns already exhausted by previous sessions (${cumulativeTurnCount}/${maxTurns})
1433
- `);
1434
- runtimeState.status = "completed";
1435
- runtimeState.runPhase = "succeeded";
1436
- runtimeState.sessionInfo.exitClassification = classifySessionExit({
1437
- runPhase: runtimeState.runPhase,
1438
- userInputRequired: false,
1439
- budgetExceeded: false,
1440
- convergenceDetected: false,
1441
- maxTurnsReached: true
1442
- });
1443
- stopOrchestratorHeartbeatTimer();
1444
- emitOrchestratorHeartbeat();
1445
- await persistSessionTokenUsageArtifact(env);
1446
- await waitForPendingOrchestratorChannelFlush(resolveTerminalOrchestratorChannelFlushTimeoutMs());
1447
- setTimeout(() => {
1448
- process.exit(0);
1449
- }, 1500);
1450
- return;
1451
- }
1452
1094
  const baseThreadParams = {
1453
1095
  cwd: plan.cwd,
1454
1096
  developerInstructions: renderedPrompt,
@@ -1458,86 +1100,70 @@ async function runCodexClientProtocol(child, plan, env, options) {
1458
1100
  mcp_servers: mcpServers
1459
1101
  }
1460
1102
  };
1461
- const resumeThreadId = plan.resumeThreadId;
1462
- let threadBootstrapMode = "fresh";
1463
- process.stderr.write(`[worker] starting codex thread (mcp_servers: ${Object.keys(mcpServers).join(", ")})
1464
- `);
1465
- let threadResult;
1466
- if (resumeThreadId) {
1467
- process.stderr.write(`[worker] attempting thread/resume for ${resumeThreadId}
1468
- `);
1469
- try {
1470
- threadResult = await sendRequestWithTimeout("thread-resume-1", "thread/resume", {
1471
- ...baseThreadParams,
1472
- threadId: resumeThreadId
1473
- });
1474
- threadBootstrapMode = "resume";
1475
- } catch (error) {
1476
- const message = error instanceof Error ? error.message : String(error ?? "unknown");
1477
- threadBootstrapMode = "soft-resume";
1478
- process.stderr.write(`[worker] thread/resume failed for ${resumeThreadId}: ${message}; falling back to thread/start
1479
- `);
1480
- threadResult = await sendRequestWithTimeout("thread-1", "thread/start", {
1481
- ...baseThreadParams,
1482
- ephemeral: false
1483
- });
1484
- }
1485
- } else {
1486
- threadResult = await sendRequestWithTimeout("thread-1", "thread/start", {
1103
+ process.stderr.write(
1104
+ `[worker] starting codex thread (mcp_servers: ${Object.keys(mcpServers).join(", ")})
1105
+ `
1106
+ );
1107
+ const threadResult = await sendRequestWithTimeout(
1108
+ "thread-1",
1109
+ "thread/start",
1110
+ {
1487
1111
  ...baseThreadParams,
1488
1112
  ephemeral: false
1489
- });
1490
- }
1113
+ }
1114
+ );
1491
1115
  const threadId = threadResult.thread_id ?? threadResult.thread?.id;
1492
1116
  runtimeState.sessionInfo.threadId = threadId ?? null;
1493
1117
  runtimeState.sessionInfo.turnId = null;
1494
1118
  runtimeState.sessionInfo.sessionId = null;
1495
1119
  runtimeState.sessionInfo.exitClassification = null;
1496
1120
  runtimeState.sessionId = null;
1497
- process.stderr.write(`[worker] codex thread started (id=${String(threadId ?? "unknown")})
1498
- `);
1121
+ process.stderr.write(
1122
+ `[worker] codex thread started (id=${String(threadId ?? "unknown")})
1123
+ `
1124
+ );
1499
1125
  if (!threadId) {
1500
- process.stderr.write("[worker] warning: no threadId returned; cannot start turn\n");
1126
+ process.stderr.write(
1127
+ "[worker] warning: no threadId returned; cannot start turn\n"
1128
+ );
1501
1129
  return;
1502
1130
  }
1503
1131
  let turnCount = 0;
1504
1132
  let requestIdCounter = 0;
1505
- let maxTurnsReached = false;
1506
- for (let turn = 0; turn < remainingTurns; turn++) {
1507
- budgetExceededReason = checkSessionBudgets(turn);
1508
- if (budgetExceededReason) {
1509
- process.stderr.write(`[worker] session budget exceeded (${budgetExceededReason}) \u2014 exiting
1510
- `);
1511
- break;
1512
- }
1133
+ let maxTurnsReached = exhaustedBeforeStart;
1134
+ if (exhaustedBeforeStart) {
1135
+ process.stderr.write(
1136
+ `[worker] max_turns (${String(env.SYMPHONY_MAX_TURNS ?? maxTurns)}) does not allow any turns for this worker session \u2014 exiting
1137
+ `
1138
+ );
1139
+ }
1140
+ for (let turn = 0; turn < maxTurns; turn++) {
1513
1141
  turnCount = turn + 1;
1514
- const globalTurnCount = cumulativeTurnCount + turnCount;
1515
1142
  runtimeState.sessionInfo.turnCount = turnCount;
1516
1143
  runtimeState.runPhase = "streaming_turn";
1517
1144
  const isFirstTurn = turn === 0;
1518
- const turnInput = isFirstTurn ? buildInitialTurnInput({
1519
- renderedPrompt,
1520
- mode: threadBootstrapMode,
1521
- lastTurnSummary,
1522
- cumulativeTurnCount,
1523
- continuationGuidance
1524
- }) : buildContinuationTurnInput({
1145
+ const turnInput = isFirstTurn ? renderedPrompt : buildContinuationTurnInput({
1525
1146
  continuationGuidance,
1526
- lastTurnSummary,
1527
- cumulativeTurnCount: globalTurnCount - 1
1147
+ cumulativeTurnCount: turn
1528
1148
  });
1529
- process.stderr.write(`[worker] starting codex turn ${globalTurnCount}/${maxTurns}${isFirstTurn ? " (initial)" : " (continuation)"}
1530
- `);
1149
+ process.stderr.write(
1150
+ `[worker] starting codex turn ${turnCount}/${maxTurns}${isFirstTurn ? " (initial)" : " (continuation)"}
1151
+ `
1152
+ );
1531
1153
  requestIdCounter += 1;
1532
1154
  const turnRequestId = `turn-${requestIdCounter}`;
1533
- const turnResult = await sendRequestWithTimeout(turnRequestId, "turn/start", {
1534
- threadId,
1535
- input: [{ type: "text", text: turnInput }],
1536
- cwd: plan.cwd,
1537
- title: composeTurnTitle(issueIdentifier, env.SYMPHONY_ISSUE_TITLE),
1538
- approvalPolicy,
1539
- sandboxPolicy: turnSandboxPolicy
1540
- });
1155
+ const turnResult = await sendRequestWithTimeout(
1156
+ turnRequestId,
1157
+ "turn/start",
1158
+ {
1159
+ threadId,
1160
+ input: [{ type: "text", text: turnInput }],
1161
+ cwd: plan.cwd,
1162
+ title: composeTurnTitle(issueIdentifier, env.SYMPHONY_ISSUE_TITLE),
1163
+ approvalPolicy,
1164
+ sandboxPolicy: turnSandboxPolicy
1165
+ }
1166
+ );
1541
1167
  const turnId = turnResult.turn_id ?? turnResult.turn?.id;
1542
1168
  const sessionId = threadId && turnId ? `${threadId}-${turnId}` : null;
1543
1169
  runtimeState.sessionInfo.turnId = turnId ?? null;
@@ -1551,10 +1177,14 @@ async function runCodexClientProtocol(child, plan, env, options) {
1551
1177
  sessionId,
1552
1178
  tokenUsageBaseline: cloneTokenUsageSnapshot()
1553
1179
  };
1554
- process.stderr.write(`[worker] codex turn started (id=${String(turnId ?? "unknown")})
1555
- `);
1556
- process.stderr.write(`[worker] session_id=${String(sessionId ?? "unknown")}
1557
- `);
1180
+ process.stderr.write(
1181
+ `[worker] codex turn started (id=${String(turnId ?? "unknown")})
1182
+ `
1183
+ );
1184
+ process.stderr.write(
1185
+ `[worker] session_id=${String(sessionId ?? "unknown")}
1186
+ `
1187
+ );
1558
1188
  emitTurnStartedEvent(activeTurnTelemetry);
1559
1189
  await waitForTurnWithTimeout();
1560
1190
  if (userInputRequired) {
@@ -1562,20 +1192,18 @@ async function runCodexClientProtocol(child, plan, env, options) {
1562
1192
  break;
1563
1193
  }
1564
1194
  if (turnTerminalFailurePhase) {
1565
- process.stderr.write(`[worker] exiting due to ${turnTerminalFailurePhase}
1566
- `);
1195
+ process.stderr.write(
1196
+ `[worker] exiting due to ${turnTerminalFailurePhase}
1197
+ `
1198
+ );
1567
1199
  break;
1568
1200
  }
1569
- budgetExceededReason = checkSessionBudgets(turnCount);
1570
- if (budgetExceededReason) {
1571
- process.stderr.write(`[worker] session budget exceeded (${budgetExceededReason}) \u2014 exiting
1572
- `);
1573
- break;
1574
- }
1575
- if (turn + 1 >= remainingTurns) {
1201
+ if (turn + 1 >= maxTurns) {
1576
1202
  maxTurnsReached = true;
1577
- process.stderr.write(`[worker] max_turns (${maxTurns}) reached across sessions \u2014 exiting
1578
- `);
1203
+ process.stderr.write(
1204
+ `[worker] max_turns (${maxTurns}) reached for this worker session \u2014 exiting
1205
+ `
1206
+ );
1579
1207
  break;
1580
1208
  }
1581
1209
  const trackerState = await refreshTrackerState(env);
@@ -1588,19 +1216,26 @@ async function runCodexClientProtocol(child, plan, env, options) {
1588
1216
  trackerState,
1589
1217
  userInputRequired: false
1590
1218
  });
1591
- process.stderr.write("[worker] issue no longer actionable \u2014 exiting multi-turn loop\n");
1219
+ process.stderr.write(
1220
+ "[worker] issue no longer actionable \u2014 exiting multi-turn loop\n"
1221
+ );
1592
1222
  break;
1593
1223
  }
1594
1224
  const currentTurnProgressSnapshot = {
1595
1225
  ...captureTurnWorkspaceSnapshot(plan.cwd),
1596
1226
  lastError: runtimeState.run?.lastError ?? null
1597
1227
  };
1598
- const turnProgress = evaluateTurnProgress(previousTurnProgressSnapshot, currentTurnProgressSnapshot);
1228
+ const turnProgress = evaluateTurnProgress(
1229
+ previousTurnProgressSnapshot,
1230
+ currentTurnProgressSnapshot
1231
+ );
1599
1232
  previousTurnProgressSnapshot = currentTurnProgressSnapshot;
1600
1233
  if (turnProgress.nonProductive) {
1601
1234
  consecutiveNonProductiveTurns += 1;
1602
- process.stderr.write(`[worker] non-productive turn detected (${consecutiveNonProductiveTurns}/${maxNonProductiveTurns})${turnProgress.reason ? `: ${turnProgress.reason}` : ""}
1603
- `);
1235
+ process.stderr.write(
1236
+ `[worker] non-productive turn detected (${consecutiveNonProductiveTurns}/${maxNonProductiveTurns})${turnProgress.reason ? `: ${turnProgress.reason}` : ""}
1237
+ `
1238
+ );
1604
1239
  } else {
1605
1240
  consecutiveNonProductiveTurns = 0;
1606
1241
  }
@@ -1609,27 +1244,33 @@ async function runCodexClientProtocol(child, plan, env, options) {
1609
1244
  if (runtimeState.run) {
1610
1245
  runtimeState.run.lastError = turnProgress.reason ? `convergence_detected: ${turnProgress.reason}` : "convergence_detected: repeated non-productive turn results";
1611
1246
  }
1612
- process.stderr.write(`[worker] convergence detected after ${consecutiveNonProductiveTurns} non-productive turns \u2014 exiting
1613
- `);
1247
+ process.stderr.write(
1248
+ `[worker] convergence detected after ${consecutiveNonProductiveTurns} non-productive turns \u2014 exiting
1249
+ `
1250
+ );
1614
1251
  break;
1615
1252
  }
1616
1253
  }
1617
- process.stderr.write(`[worker] multi-turn loop complete after ${turnCount} turn(s) \u2014 exiting worker
1618
- `);
1254
+ process.stderr.write(
1255
+ `[worker] multi-turn loop complete after ${turnCount} turn(s) \u2014 exiting worker
1256
+ `
1257
+ );
1619
1258
  runtimeState.runPhase = "finishing";
1620
1259
  runtimeState.status = userInputRequired || turnTerminalFailurePhase ? "failed" : "completed";
1621
1260
  runtimeState.runPhase = convergenceDetected ? "failed" : userInputRequired ? "failed" : turnTerminalFailurePhase ?? "succeeded";
1622
1261
  runtimeState.sessionInfo.exitClassification = classifySessionExit({
1623
1262
  runPhase: runtimeState.runPhase,
1624
1263
  userInputRequired,
1625
- budgetExceeded: budgetExceededReason !== null,
1264
+ budgetExceeded: false,
1626
1265
  convergenceDetected,
1627
1266
  maxTurnsReached
1628
1267
  });
1629
1268
  stopOrchestratorHeartbeatTimer();
1630
1269
  emitOrchestratorHeartbeat();
1631
1270
  await persistSessionTokenUsageArtifact(env);
1632
- await waitForPendingOrchestratorChannelFlush(resolveTerminalOrchestratorChannelFlushTimeoutMs());
1271
+ await waitForPendingOrchestratorChannelFlush(
1272
+ resolveTerminalOrchestratorChannelFlushTimeoutMs()
1273
+ );
1633
1274
  setTimeout(() => {
1634
1275
  process.exit(userInputRequired || turnTerminalFailurePhase ? 1 : 0);
1635
1276
  }, 1500);
@@ -1661,13 +1302,18 @@ async function runCodexClientProtocol(child, plan, env, options) {
1661
1302
  maxTurnsReached: false
1662
1303
  });
1663
1304
  if (activeTurnTelemetry) {
1664
- emitTurnFailedEvent(activeTurnTelemetry, runtimeState.run?.lastError ?? errMsg);
1305
+ emitTurnFailedEvent(
1306
+ activeTurnTelemetry,
1307
+ runtimeState.run?.lastError ?? errMsg
1308
+ );
1665
1309
  activeTurnTelemetry = null;
1666
1310
  }
1667
1311
  stopOrchestratorHeartbeatTimer();
1668
1312
  emitOrchestratorHeartbeat();
1669
1313
  await persistSessionTokenUsageArtifact(env);
1670
- await waitForPendingOrchestratorChannelFlush(resolveTerminalOrchestratorChannelFlushTimeoutMs());
1314
+ await waitForPendingOrchestratorChannelFlush(
1315
+ resolveTerminalOrchestratorChannelFlushTimeoutMs()
1316
+ );
1671
1317
  setTimeout(() => {
1672
1318
  process.exit(1);
1673
1319
  }, 1500);
@@ -1677,16 +1323,20 @@ function applyTokenUsageUpdate(source, tokenUsage) {
1677
1323
  runtimeState.tokenUsage.inputTokens = tokenUsage.inputTokens;
1678
1324
  runtimeState.tokenUsage.outputTokens = tokenUsage.outputTokens;
1679
1325
  runtimeState.tokenUsage.totalTokens = tokenUsage.totalTokens;
1680
- process.stderr.write(`[worker] token_usage source=${source} input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens}
1681
- `);
1326
+ process.stderr.write(
1327
+ `[worker] token_usage source=${source} input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens}
1328
+ `
1329
+ );
1682
1330
  }
1683
1331
  function applyRateLimitUpdate(source, rateLimits) {
1684
1332
  runtimeState.rateLimits = {
1685
1333
  ...rateLimits,
1686
1334
  source: "codex"
1687
1335
  };
1688
- process.stderr.write(`[worker] rate_limits source=${source} payload=${JSON.stringify(runtimeState.rateLimits).slice(0, 300)}
1689
- `);
1336
+ process.stderr.write(
1337
+ `[worker] rate_limits source=${source} payload=${JSON.stringify(runtimeState.rateLimits).slice(0, 300)}
1338
+ `
1339
+ );
1690
1340
  }
1691
1341
  function extractRateLimitPayload(value) {
1692
1342
  if (!value || typeof value !== "object") {
@@ -1817,22 +1467,23 @@ async function refreshTrackerState(env) {
1817
1467
  }
1818
1468
  try {
1819
1469
  const response = await fetch(`${orchestratorUrl}/api/v1/state`);
1820
- if (!response.ok)
1821
- return "unknown";
1470
+ if (!response.ok) return "unknown";
1822
1471
  const status = await response.json();
1823
- const isActive = status.activeRuns?.some((run) => run.issueIdentifier === issueIdentifier);
1472
+ const isActive = status.activeRuns?.some(
1473
+ (run) => run.issueIdentifier === issueIdentifier
1474
+ );
1824
1475
  return isActive ? "active" : "non-actionable";
1825
1476
  } catch {
1826
1477
  return "unknown";
1827
1478
  }
1828
1479
  }
1829
1480
  function runToolProcess(toolDef, inputJson) {
1830
- return new Promise((resolve2, reject) => {
1481
+ return new Promise((resolve, reject) => {
1831
1482
  const toolEnv = {
1832
1483
  ...process.env,
1833
1484
  ...toolDef.env
1834
1485
  };
1835
- const toolProc = spawn2(toolDef.command, toolDef.args, {
1486
+ const toolProc = spawn(toolDef.command, toolDef.args, {
1836
1487
  env: toolEnv,
1837
1488
  stdio: "pipe"
1838
1489
  });
@@ -1844,10 +1495,14 @@ function runToolProcess(toolDef, inputJson) {
1844
1495
  toolProc.once("exit", (code) => {
1845
1496
  const output = Buffer.concat(stdout).toString("utf8").trim();
1846
1497
  if (code === 0) {
1847
- resolve2(output || "{}");
1498
+ resolve(output || "{}");
1848
1499
  } else {
1849
1500
  const errOutput = Buffer.concat(stderr).toString("utf8").trim();
1850
- reject(new Error(`Tool exited with code ${code ?? "unknown"}: ${errOutput || output}`));
1501
+ reject(
1502
+ new Error(
1503
+ `Tool exited with code ${code ?? "unknown"}: ${errOutput || output}`
1504
+ )
1505
+ );
1851
1506
  }
1852
1507
  });
1853
1508
  toolProc.stdin?.write(inputJson);