@gh-symphony/cli 0.0.15 → 0.0.17

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.
@@ -0,0 +1,1828 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ classifySessionExit,
4
+ parseWorkflowMarkdown,
5
+ readEnvFile
6
+ } from "./chunk-TF3QNWNC.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";
12
+
13
+ // ../runtime-codex/dist/runtime.js
14
+ 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
+ }
446
+
447
+ // ../worker/dist/execution-phase.js
448
+ function resolveInitialExecutionPhase(input) {
449
+ const { issueState, blockerCheckStates, activeStates } = input;
450
+ if (!issueState) {
451
+ return null;
452
+ }
453
+ if (blockerCheckStates.includes(issueState)) {
454
+ return "planning";
455
+ }
456
+ if (activeStates.includes(issueState)) {
457
+ return "implementation";
458
+ }
459
+ return null;
460
+ }
461
+ function resolvePausedExecutionPhase(currentPhase) {
462
+ if (currentPhase === "planning") {
463
+ return "human-review";
464
+ }
465
+ if (currentPhase === "implementation") {
466
+ return "awaiting-merge";
467
+ }
468
+ return null;
469
+ }
470
+ function resolveFinalExecutionPhase(input) {
471
+ if (input.userInputRequired || input.trackerState !== "non-actionable") {
472
+ return input.currentPhase;
473
+ }
474
+ return resolvePausedExecutionPhase(input.currentPhase) ?? input.currentPhase;
475
+ }
476
+
477
+ // ../worker/dist/codex-policy.js
478
+ function resolveCodexPolicySettings(env) {
479
+ return {
480
+ approvalPolicy: env.SYMPHONY_APPROVAL_POLICY || "never",
481
+ threadSandbox: env.SYMPHONY_THREAD_SANDBOX || "danger-full-access",
482
+ turnSandboxPolicy: env.SYMPHONY_TURN_SANDBOX_POLICY ? { type: env.SYMPHONY_TURN_SANDBOX_POLICY } : void 0
483
+ };
484
+ }
485
+
486
+ // ../worker/dist/convergence-detection.js
487
+ import { spawnSync } from "child_process";
488
+ var DEFAULT_MAX_NONPRODUCTIVE_TURNS = 3;
489
+ function resolveMaxNonProductiveTurns(env) {
490
+ const rawValue = env.SYMPHONY_MAX_NONPRODUCTIVE_TURNS;
491
+ const parsed = Number(rawValue);
492
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_NONPRODUCTIVE_TURNS;
493
+ }
494
+ function captureTurnWorkspaceSnapshot(cwd) {
495
+ const result = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
496
+ cwd,
497
+ encoding: "utf8"
498
+ });
499
+ if (result.status !== 0) {
500
+ return {
501
+ fingerprint: null,
502
+ changedFiles: []
503
+ };
504
+ }
505
+ const changedFiles = result.stdout.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean).sort();
506
+ return {
507
+ fingerprint: changedFiles.join("\n"),
508
+ changedFiles
509
+ };
510
+ }
511
+ function evaluateTurnProgress(previous, current) {
512
+ const normalizedPreviousError = normalizeError(previous.lastError);
513
+ const normalizedCurrentError = normalizeError(current.lastError);
514
+ const repeatedError = normalizedPreviousError !== null && normalizedCurrentError !== null && normalizedPreviousError === normalizedCurrentError;
515
+ if (repeatedError) {
516
+ return {
517
+ nonProductive: true,
518
+ repeatedPattern: true,
519
+ reason: `repeated error: ${normalizedCurrentError}`
520
+ };
521
+ }
522
+ const unchangedWorkspace = previous.fingerprint !== null && current.fingerprint !== null && previous.fingerprint === current.fingerprint;
523
+ if (unchangedWorkspace) {
524
+ return {
525
+ nonProductive: true,
526
+ repeatedPattern: true,
527
+ reason: current.changedFiles.length > 0 ? `workspace diff unchanged (${current.changedFiles.length} tracked change${current.changedFiles.length === 1 ? "" : "s"})` : "workspace unchanged"
528
+ };
529
+ }
530
+ return {
531
+ nonProductive: false,
532
+ repeatedPattern: false,
533
+ reason: null
534
+ };
535
+ }
536
+ function normalizeError(value) {
537
+ if (typeof value !== "string") {
538
+ return null;
539
+ }
540
+ const normalized = value.trim();
541
+ return normalized.length > 0 ? normalized : null;
542
+ }
543
+
544
+ // ../worker/dist/run-phase.js
545
+ var TERMINAL_RUN_PHASES = /* @__PURE__ */ new Set([
546
+ "succeeded",
547
+ "failed",
548
+ "timed_out",
549
+ "stalled",
550
+ "canceled_by_reconciliation"
551
+ ]);
552
+ function resolveExitRunPhase(currentRunPhase, exit) {
553
+ if (currentRunPhase && TERMINAL_RUN_PHASES.has(currentRunPhase)) {
554
+ return currentRunPhase;
555
+ }
556
+ return exit.code === 0 && !exit.signal ? "succeeded" : "failed";
557
+ }
558
+
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
617
+ var DEFAULT_CONTINUATION_GUIDANCE = "Continue working on the issue. Review your progress and complete any remaining tasks.";
618
+ function parseNonNegativeInteger2(value) {
619
+ const parsed = typeof value === "number" ? value : Number.parseInt(value ?? "", 10);
620
+ if (!Number.isFinite(parsed) || parsed <= 0) {
621
+ return 0;
622
+ }
623
+ return Math.floor(parsed);
624
+ }
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 }) {
660
+ const template = continuationGuidance?.trim() || DEFAULT_CONTINUATION_GUIDANCE;
661
+ return renderContinuationGuidance(template, {
662
+ lastTurnSummary: normalizeContinuationVariable(lastTurnSummary) ?? "No previous turn summary was captured.",
663
+ cumulativeTurnCount: String(Math.max(0, parseNonNegativeInteger2(cumulativeTurnCount)))
664
+ });
665
+ }
666
+ function normalizeContinuationVariable(value) {
667
+ const normalized = value?.trim();
668
+ return normalized ? normalized : null;
669
+ }
670
+ function renderContinuationGuidance(template, variables) {
671
+ return template.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, (match, key) => variables[key] ?? match);
672
+ }
673
+
674
+ // ../worker/dist/token-usage.js
675
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
676
+ import { dirname as dirname2, join as join2 } from "path";
677
+ async function persistTokenUsageArtifact(env, tokenUsage) {
678
+ const artifactPath = resolveTokenUsageArtifactPath(env);
679
+ if (!artifactPath) {
680
+ return;
681
+ }
682
+ try {
683
+ await mkdir2(dirname2(artifactPath), { recursive: true });
684
+ await writeFile3(artifactPath, JSON.stringify(tokenUsage, null, 2) + "\n", "utf8");
685
+ } catch (error) {
686
+ const message = error instanceof Error ? error.message : String(error);
687
+ process.stderr.write(`[worker] failed to persist token usage artifact: ${message}
688
+ `);
689
+ }
690
+ }
691
+ function resolveTokenUsageArtifactPath(env) {
692
+ const workspaceRuntimeDir = env.WORKSPACE_RUNTIME_DIR;
693
+ if (!workspaceRuntimeDir) {
694
+ return null;
695
+ }
696
+ return join2(workspaceRuntimeDir, "token-usage.json");
697
+ }
698
+
699
+ // ../worker/dist/index.js
700
+ var launcherEnv = loadLauncherEnvironment(process.env);
701
+ var sessionBudgetState = resolveSessionBudgetState(launcherEnv);
702
+ var runtimeState = {
703
+ status: launcherEnv.SYMPHONY_RUN_ID ? "starting" : "idle",
704
+ executionPhase: null,
705
+ runPhase: launcherEnv.SYMPHONY_RUN_ID ? "preparing_workspace" : null,
706
+ sessionId: null,
707
+ run: launcherEnv.SYMPHONY_RUN_ID ? {
708
+ runId: launcherEnv.SYMPHONY_RUN_ID,
709
+ issueId: launcherEnv.SYMPHONY_ISSUE_ID ?? null,
710
+ issueIdentifier: launcherEnv.SYMPHONY_ISSUE_IDENTIFIER ?? null,
711
+ state: launcherEnv.SYMPHONY_ISSUE_STATE ?? null,
712
+ processId: null,
713
+ repository: {
714
+ owner: launcherEnv.TARGET_REPOSITORY_OWNER ?? null,
715
+ name: launcherEnv.TARGET_REPOSITORY_NAME ?? null,
716
+ cloneUrl: launcherEnv.TARGET_REPOSITORY_CLONE_URL ?? null,
717
+ url: launcherEnv.TARGET_REPOSITORY_URL ?? null
718
+ },
719
+ lastError: null
720
+ } : null,
721
+ tokenUsage: {
722
+ inputTokens: sessionBudgetState.tokenUsageBaseline.inputTokens,
723
+ outputTokens: sessionBudgetState.tokenUsageBaseline.outputTokens,
724
+ totalTokens: sessionBudgetState.tokenUsageBaseline.totalTokens
725
+ },
726
+ lastEventAt: null,
727
+ rateLimits: null,
728
+ sessionInfo: {
729
+ threadId: null,
730
+ turnId: null,
731
+ turnCount: 0,
732
+ sessionId: null,
733
+ exitClassification: null
734
+ }
735
+ };
736
+ console.log(JSON.stringify({
737
+ package: "@gh-symphony/worker",
738
+ runtime: "self-hosted-sample"
739
+ }, null, 2));
740
+ var childProcess = null;
741
+ var shutdownPromise = null;
742
+ var orchestratorChannelDrainPending = false;
743
+ var pendingOrchestratorChannelPayloads = [];
744
+ var orchestratorHeartbeatTimer = null;
745
+ var MAX_PENDING_ORCHESTRATOR_CHANNEL_PAYLOADS = 16;
746
+ var ORCHESTRATOR_CHANNEL_FLUSH_TIMEOUT_MS = 250;
747
+ var ORCHESTRATOR_CHANNEL_HEARTBEAT_INTERVAL_MS = 1e4;
748
+ function composeTurnTitle(issueIdentifierValue, issueTitleValue) {
749
+ const issueIdentifier = issueIdentifierValue?.trim() ?? "";
750
+ const issueTitle = issueTitleValue?.trim() ?? "";
751
+ if (issueIdentifier && issueTitle) {
752
+ return `${issueIdentifier}: ${issueTitle}`;
753
+ }
754
+ return issueIdentifier || issueTitle || "Untitled issue";
755
+ }
756
+ if (launcherEnv.SYMPHONY_RUN_ID && launcherEnv.WORKING_DIRECTORY) {
757
+ startOrchestratorHeartbeatTimer();
758
+ void startAssignedRun();
759
+ }
760
+ function shutdown(signal) {
761
+ if (shutdownPromise) {
762
+ return;
763
+ }
764
+ shutdownPromise = (async () => {
765
+ if (childProcess?.pid) {
766
+ try {
767
+ process.kill(childProcess.pid, "SIGTERM");
768
+ } catch {
769
+ }
770
+ }
771
+ stopOrchestratorHeartbeatTimer();
772
+ emitOrchestratorHeartbeat();
773
+ await persistTokenUsageArtifact(launcherEnv, runtimeState.tokenUsage);
774
+ await waitForPendingOrchestratorChannelFlush(resolveTerminalOrchestratorChannelFlushTimeoutMs());
775
+ console.log(`Worker stopped on ${signal}`);
776
+ process.exit(0);
777
+ })();
778
+ }
779
+ process.on("SIGINT", () => shutdown("SIGINT"));
780
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
781
+ function enqueuePendingOrchestratorChannelPayload(payload) {
782
+ if (pendingOrchestratorChannelPayloads.length >= MAX_PENDING_ORCHESTRATOR_CHANNEL_PAYLOADS) {
783
+ pendingOrchestratorChannelPayloads.shift();
784
+ }
785
+ pendingOrchestratorChannelPayloads.push(payload);
786
+ }
787
+ function flushPendingOrchestratorChannelEvent() {
788
+ while (pendingOrchestratorChannelPayloads.length > 0) {
789
+ const payload = pendingOrchestratorChannelPayloads.shift();
790
+ if (!payload) {
791
+ continue;
792
+ }
793
+ const wrote = process.stderr.write(payload);
794
+ if (wrote) {
795
+ continue;
796
+ }
797
+ orchestratorChannelDrainPending = true;
798
+ process.stderr.once("drain", flushPendingOrchestratorChannelEvent);
799
+ return;
800
+ }
801
+ orchestratorChannelDrainPending = false;
802
+ }
803
+ function waitForPendingOrchestratorChannelFlush(timeoutMs = ORCHESTRATOR_CHANNEL_FLUSH_TIMEOUT_MS) {
804
+ if (!orchestratorChannelDrainPending && pendingOrchestratorChannelPayloads.length === 0) {
805
+ return Promise.resolve();
806
+ }
807
+ return new Promise((resolve2) => {
808
+ let settled = false;
809
+ let timeout = setTimeout(() => {
810
+ settled = true;
811
+ process.stderr.removeListener("drain", handleDrain);
812
+ timeout = null;
813
+ resolve2();
814
+ }, timeoutMs);
815
+ const handleDrain = () => {
816
+ if (orchestratorChannelDrainPending || pendingOrchestratorChannelPayloads.length > 0) {
817
+ return;
818
+ }
819
+ if (settled) {
820
+ return;
821
+ }
822
+ settled = true;
823
+ if (timeout) {
824
+ clearTimeout(timeout);
825
+ timeout = null;
826
+ }
827
+ process.stderr.removeListener("drain", handleDrain);
828
+ resolve2();
829
+ };
830
+ process.stderr.on("drain", handleDrain);
831
+ });
832
+ }
833
+ function resolveTerminalOrchestratorChannelFlushTimeoutMs() {
834
+ const pendingPayloadCount = pendingOrchestratorChannelPayloads.length + (orchestratorChannelDrainPending ? 1 : 0);
835
+ return Math.max(ORCHESTRATOR_CHANNEL_FLUSH_TIMEOUT_MS, pendingPayloadCount * ORCHESTRATOR_CHANNEL_FLUSH_TIMEOUT_MS);
836
+ }
837
+ function writeOrQueueOrchestratorChannelPayload(serializedPayload) {
838
+ if (orchestratorChannelDrainPending) {
839
+ enqueuePendingOrchestratorChannelPayload(serializedPayload);
840
+ return;
841
+ }
842
+ const wrote = process.stderr.write(serializedPayload);
843
+ if (!wrote) {
844
+ orchestratorChannelDrainPending = true;
845
+ process.stderr.once("drain", flushPendingOrchestratorChannelEvent);
846
+ }
847
+ }
848
+ function emitOrchestratorHeartbeat() {
849
+ const issueId = runtimeState.run?.issueId;
850
+ if (!issueId) {
851
+ return;
852
+ }
853
+ const payload = {
854
+ type: "heartbeat",
855
+ issueId,
856
+ lastEventAt: runtimeState.lastEventAt,
857
+ tokenUsage: { ...runtimeState.tokenUsage },
858
+ rateLimits: runtimeState.rateLimits ? { ...runtimeState.rateLimits } : null,
859
+ sessionInfo: { ...runtimeState.sessionInfo },
860
+ executionPhase: runtimeState.executionPhase,
861
+ runPhase: runtimeState.runPhase,
862
+ lastError: runtimeState.run?.lastError ?? null
863
+ };
864
+ writeOrQueueOrchestratorChannelPayload(`${JSON.stringify(payload)}
865
+ `);
866
+ }
867
+ function startOrchestratorHeartbeatTimer() {
868
+ if (orchestratorHeartbeatTimer) {
869
+ return;
870
+ }
871
+ orchestratorHeartbeatTimer = setInterval(() => {
872
+ emitOrchestratorHeartbeat();
873
+ }, ORCHESTRATOR_CHANNEL_HEARTBEAT_INTERVAL_MS);
874
+ orchestratorHeartbeatTimer.unref?.();
875
+ }
876
+ function stopOrchestratorHeartbeatTimer() {
877
+ if (!orchestratorHeartbeatTimer) {
878
+ return;
879
+ }
880
+ clearInterval(orchestratorHeartbeatTimer);
881
+ orchestratorHeartbeatTimer = null;
882
+ }
883
+ function emitOrchestratorChannelEvent(event) {
884
+ const issueId = runtimeState.run?.issueId;
885
+ const lastEventAt = runtimeState.lastEventAt;
886
+ if (!issueId || !lastEventAt) {
887
+ return;
888
+ }
889
+ const payload = {
890
+ type: "codex_update",
891
+ issueId,
892
+ lastEventAt,
893
+ tokenUsage: { ...runtimeState.tokenUsage },
894
+ sessionInfo: { ...runtimeState.sessionInfo },
895
+ executionPhase: runtimeState.executionPhase,
896
+ runPhase: runtimeState.runPhase,
897
+ lastError: runtimeState.run?.lastError ?? null
898
+ };
899
+ if (runtimeState.rateLimits) {
900
+ payload.rateLimits = { ...runtimeState.rateLimits };
901
+ }
902
+ if (event) {
903
+ payload.event = event;
904
+ }
905
+ writeOrQueueOrchestratorChannelPayload(`${JSON.stringify(payload)}
906
+ `);
907
+ }
908
+ function cloneTokenUsageSnapshot() {
909
+ return { ...runtimeState.tokenUsage };
910
+ }
911
+ function resolveTurnTokenUsageDelta(baseline) {
912
+ return {
913
+ inputTokens: Math.max(0, runtimeState.tokenUsage.inputTokens - baseline.inputTokens),
914
+ outputTokens: Math.max(0, runtimeState.tokenUsage.outputTokens - baseline.outputTokens),
915
+ totalTokens: Math.max(0, runtimeState.tokenUsage.totalTokens - baseline.totalTokens)
916
+ };
917
+ }
918
+ function emitTurnStartedEvent(turn) {
919
+ const issueId = runtimeState.run?.issueId;
920
+ if (!issueId) {
921
+ return;
922
+ }
923
+ const payload = {
924
+ type: "turn_started",
925
+ issueId,
926
+ startedAt: turn.startedAt,
927
+ threadId: turn.threadId,
928
+ turnId: turn.turnId,
929
+ turnCount: turn.turnCount,
930
+ sessionId: turn.sessionId
931
+ };
932
+ writeOrQueueOrchestratorChannelPayload(`${JSON.stringify(payload)}
933
+ `);
934
+ }
935
+ function emitTurnCompletedEvent(turn) {
936
+ const issueId = runtimeState.run?.issueId;
937
+ if (!issueId) {
938
+ return;
939
+ }
940
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
941
+ const payload = {
942
+ type: "turn_completed",
943
+ issueId,
944
+ startedAt: turn.startedAt,
945
+ completedAt,
946
+ durationMs: Math.max(0, new Date(completedAt).getTime() - new Date(turn.startedAt).getTime()),
947
+ threadId: turn.threadId,
948
+ turnId: turn.turnId,
949
+ turnCount: turn.turnCount,
950
+ sessionId: turn.sessionId,
951
+ tokenUsage: resolveTurnTokenUsageDelta(turn.tokenUsageBaseline)
952
+ };
953
+ writeOrQueueOrchestratorChannelPayload(`${JSON.stringify(payload)}
954
+ `);
955
+ }
956
+ function emitTurnFailedEvent(turn, error) {
957
+ const issueId = runtimeState.run?.issueId;
958
+ if (!issueId) {
959
+ return;
960
+ }
961
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
962
+ const payload = {
963
+ type: "turn_failed",
964
+ issueId,
965
+ startedAt: turn.startedAt,
966
+ failedAt,
967
+ durationMs: Math.max(0, new Date(failedAt).getTime() - new Date(turn.startedAt).getTime()),
968
+ threadId: turn.threadId,
969
+ turnId: turn.turnId,
970
+ turnCount: turn.turnCount,
971
+ sessionId: turn.sessionId,
972
+ tokenUsage: resolveTurnTokenUsageDelta(turn.tokenUsageBaseline),
973
+ error
974
+ };
975
+ writeOrQueueOrchestratorChannelPayload(`${JSON.stringify(payload)}
976
+ `);
977
+ }
978
+ async function startAssignedRun() {
979
+ try {
980
+ const workflowPath = launcherEnv.SYMPHONY_WORKFLOW_PATH || join3(launcherEnv.WORKING_DIRECTORY, "WORKFLOW.md");
981
+ runtimeState.runPhase = "building_prompt";
982
+ const workflow = parseWorkflowMarkdown(await readFile2(workflowPath, "utf8"), launcherEnv);
983
+ runtimeState.executionPhase = resolveInitialExecutionPhase({
984
+ issueState: runtimeState.run?.state,
985
+ blockerCheckStates: workflow.lifecycle.blockerCheckStates,
986
+ activeStates: workflow.lifecycle.activeStates
987
+ });
988
+ const config = resolveLocalRuntimeLaunchConfig(launcherEnv);
989
+ config.agentCommand = workflow.codex.command;
990
+ runtimeState.runPhase = "launching_agent";
991
+ const plan = await prepareCodexRuntimePlan(config);
992
+ childProcess = launchCodexAppServer(plan);
993
+ runtimeState.status = "running";
994
+ runtimeState.runPhase = "initializing_session";
995
+ if (runtimeState.run) {
996
+ runtimeState.run.processId = childProcess.pid ?? null;
997
+ }
998
+ void runCodexClientProtocol(childProcess, plan, launcherEnv, {
999
+ continuationGuidance: workflow.continuationGuidance
1000
+ });
1001
+ childProcess.once("exit", (code, signal) => {
1002
+ const currentRunPhase = runtimeState.runPhase;
1003
+ const nextRunPhase = resolveExitRunPhase(currentRunPhase, {
1004
+ code,
1005
+ signal
1006
+ });
1007
+ const preservesTerminalPhase = currentRunPhase != null && nextRunPhase === currentRunPhase;
1008
+ if (!preservesTerminalPhase) {
1009
+ runtimeState.status = code === 0 && !signal ? "completed" : "failed";
1010
+ }
1011
+ runtimeState.runPhase = nextRunPhase;
1012
+ if (runtimeState.run) {
1013
+ if (!preservesTerminalPhase) {
1014
+ runtimeState.run.lastError = code === 0 && !signal ? null : `codex app-server exited with ${signal ?? code ?? "unknown"}`;
1015
+ }
1016
+ }
1017
+ void persistTokenUsageArtifact(launcherEnv, runtimeState.tokenUsage);
1018
+ });
1019
+ childProcess.once("error", (error) => {
1020
+ runtimeState.status = "failed";
1021
+ runtimeState.runPhase = "failed";
1022
+ if (runtimeState.run) {
1023
+ runtimeState.run.lastError = error.message;
1024
+ }
1025
+ void persistTokenUsageArtifact(launcherEnv, runtimeState.tokenUsage);
1026
+ });
1027
+ } catch (error) {
1028
+ runtimeState.status = "failed";
1029
+ runtimeState.runPhase = "failed";
1030
+ if (runtimeState.run) {
1031
+ runtimeState.run.lastError = error instanceof Error ? error.message : "Unknown worker startup error";
1032
+ }
1033
+ await persistTokenUsageArtifact(launcherEnv, runtimeState.tokenUsage);
1034
+ }
1035
+ }
1036
+ async function runCodexClientProtocol(child, plan, env, options) {
1037
+ const renderedPrompt = env.SYMPHONY_RENDERED_PROMPT;
1038
+ if (!renderedPrompt) {
1039
+ process.stderr.write("[worker] SYMPHONY_RENDERED_PROMPT not set; skipping codex client protocol\n");
1040
+ return;
1041
+ }
1042
+ if (!child.stdin || !child.stdout) {
1043
+ process.stderr.write("[worker] codex process has no stdio pipes; cannot run client protocol\n");
1044
+ return;
1045
+ }
1046
+ const maxTurns = Number(env.SYMPHONY_MAX_TURNS) || 20;
1047
+ const cumulativeTurnCount = parseNonNegativeInteger2(env.SYMPHONY_CUMULATIVE_TURN_COUNT);
1048
+ const remainingTurns = resolveRemainingTurns(maxTurns, cumulativeTurnCount);
1049
+ const readTimeoutMs = Number(env.SYMPHONY_READ_TIMEOUT_MS) || 5e3;
1050
+ const turnTimeoutMs = Number(env.SYMPHONY_TURN_TIMEOUT_MS) || 36e5;
1051
+ const maxNonProductiveTurns = resolveMaxNonProductiveTurns(env);
1052
+ const issueIdentifier = env.SYMPHONY_ISSUE_IDENTIFIER ?? "";
1053
+ const lastTurnSummary = env.SYMPHONY_LAST_TURN_SUMMARY ?? null;
1054
+ const continuationGuidance = env.SYMPHONY_CONTINUATION_GUIDANCE ?? options.continuationGuidance;
1055
+ const { approvalPolicy, threadSandbox, turnSandboxPolicy } = resolveCodexPolicySettings(env);
1056
+ const budgetState = resolveSessionBudgetState(env);
1057
+ let previousTurnProgressSnapshot = {
1058
+ ...captureTurnWorkspaceSnapshot(plan.cwd),
1059
+ lastError: runtimeState.run?.lastError ?? null
1060
+ };
1061
+ child.stderr?.pipe(process.stderr);
1062
+ let lineBuffer = "";
1063
+ let deltaBuffer = null;
1064
+ function flushDeltaBuffer() {
1065
+ if (!deltaBuffer)
1066
+ return;
1067
+ process.stderr.write(`[worker] codex \u2192 agent_message [accumulated] ${JSON.stringify({ text: deltaBuffer.text }).slice(0, 500)}
1068
+ `);
1069
+ deltaBuffer = null;
1070
+ }
1071
+ const pendingRequests = /* @__PURE__ */ new Map();
1072
+ let turnCompletedResolve = null;
1073
+ let userInputRequired = false;
1074
+ let turnTerminalFailurePhase = null;
1075
+ let activeTurnTelemetry = null;
1076
+ let budgetExceededReason = null;
1077
+ let consecutiveNonProductiveTurns = 0;
1078
+ let convergenceDetected = false;
1079
+ function checkSessionBudgets(currentSessionTurnCount) {
1080
+ return resolveBudgetExceededReason(budgetState, currentSessionTurnCount, {
1081
+ inputTokens: runtimeState.tokenUsage.inputTokens - budgetState.tokenUsageBaseline.inputTokens,
1082
+ outputTokens: runtimeState.tokenUsage.outputTokens - budgetState.tokenUsageBaseline.outputTokens,
1083
+ totalTokens: runtimeState.tokenUsage.totalTokens - budgetState.tokenUsageBaseline.totalTokens
1084
+ }, /* @__PURE__ */ new Date());
1085
+ }
1086
+ function resolvePendingTurnCompletion() {
1087
+ if (turnCompletedResolve) {
1088
+ turnCompletedResolve();
1089
+ turnCompletedResolve = null;
1090
+ }
1091
+ }
1092
+ function describeTurnTerminalEvent(event, params) {
1093
+ const fallback = event === "turn/failed" ? "turn_failed: codex reported turn failure" : "turn_cancelled: codex reported turn cancellation";
1094
+ if (!params || typeof params !== "object") {
1095
+ return fallback;
1096
+ }
1097
+ const record = params;
1098
+ const directReasonKeys = ["message", "reason", "error"];
1099
+ for (const key of directReasonKeys) {
1100
+ const value = record[key];
1101
+ if (typeof value === "string" && value.trim()) {
1102
+ return `${event.replace("/", "_")}: ${value.trim()}`;
1103
+ }
1104
+ if (value && typeof value === "object" && typeof value.message === "string") {
1105
+ const nested = value;
1106
+ const nestedMessage = String(nested.message).trim();
1107
+ if (nestedMessage) {
1108
+ return `${event.replace("/", "_")}: ${nestedMessage}`;
1109
+ }
1110
+ }
1111
+ }
1112
+ const serialized = JSON.stringify(params).slice(0, 300);
1113
+ return serialized && serialized !== "{}" ? `${event.replace("/", "_")}: ${serialized}` : fallback;
1114
+ }
1115
+ function markTurnTerminalFailure(runPhase, lastError) {
1116
+ runtimeState.status = "failed";
1117
+ runtimeState.runPhase = runPhase;
1118
+ if (runtimeState.run) {
1119
+ runtimeState.run.lastError = lastError;
1120
+ }
1121
+ turnTerminalFailurePhase = runPhase;
1122
+ resolvePendingTurnCompletion();
1123
+ if (activeTurnTelemetry) {
1124
+ emitTurnFailedEvent(activeTurnTelemetry, lastError);
1125
+ activeTurnTelemetry = null;
1126
+ }
1127
+ }
1128
+ function sendMessage(msg) {
1129
+ const line = JSON.stringify(msg) + "\n";
1130
+ child.stdin?.write(line);
1131
+ }
1132
+ function sendRequest(id, method, params) {
1133
+ return new Promise((resolve2, reject) => {
1134
+ pendingRequests.set(id, { resolve: resolve2, reject });
1135
+ sendMessage({ jsonrpc: "2.0", id, method, params });
1136
+ });
1137
+ }
1138
+ function sendRequestWithTimeout(id, method, params) {
1139
+ return new Promise((resolve2, reject) => {
1140
+ const timer = setTimeout(() => {
1141
+ pendingRequests.delete(id);
1142
+ reject(new Error(`response_timeout: ${method} timed out after ${readTimeoutMs}ms`));
1143
+ }, readTimeoutMs);
1144
+ sendRequest(id, method, params).then((result) => {
1145
+ clearTimeout(timer);
1146
+ resolve2(result);
1147
+ }, (error) => {
1148
+ clearTimeout(timer);
1149
+ reject(error);
1150
+ });
1151
+ });
1152
+ }
1153
+ function waitForTurnCompletion() {
1154
+ return new Promise((resolve2) => {
1155
+ turnCompletedResolve = resolve2;
1156
+ });
1157
+ }
1158
+ function waitForTurnWithTimeout() {
1159
+ return new Promise((resolve2, reject) => {
1160
+ const timer = setTimeout(() => {
1161
+ process.stderr.write(`[worker] turn_timeout: turn exceeded ${turnTimeoutMs}ms \u2014 killing codex process
1162
+ `);
1163
+ if (child.pid) {
1164
+ try {
1165
+ process.kill(child.pid, "SIGTERM");
1166
+ } catch {
1167
+ }
1168
+ }
1169
+ reject(new Error("turn_timeout: turn exceeded time limit"));
1170
+ }, turnTimeoutMs);
1171
+ waitForTurnCompletion().then(() => {
1172
+ clearTimeout(timer);
1173
+ resolve2();
1174
+ }, (error) => {
1175
+ clearTimeout(timer);
1176
+ reject(error);
1177
+ });
1178
+ });
1179
+ }
1180
+ async function dispatchDynamicToolCall(callId, toolName, threadId, turnId, args) {
1181
+ const toolDef = plan.tools.find((t) => t.name === toolName);
1182
+ if (!toolDef) {
1183
+ process.stderr.write(`[worker] unknown dynamic tool: ${toolName}; sending error response
1184
+ `);
1185
+ sendMessage({
1186
+ jsonrpc: "2.0",
1187
+ method: "dynamic_tool_call_response",
1188
+ params: {
1189
+ callId,
1190
+ threadId,
1191
+ turnId,
1192
+ contentItems: [
1193
+ {
1194
+ type: "input_text",
1195
+ text: `Tool "${toolName}" is not registered.`
1196
+ }
1197
+ ],
1198
+ isError: true
1199
+ }
1200
+ });
1201
+ return;
1202
+ }
1203
+ const inputJson = JSON.stringify(args ?? {});
1204
+ process.stderr.write(`[worker] executing dynamic tool "${toolName}" (callId=${callId})
1205
+ `);
1206
+ try {
1207
+ const output = await runToolProcess(toolDef, inputJson);
1208
+ sendMessage({
1209
+ jsonrpc: "2.0",
1210
+ method: "dynamic_tool_call_response",
1211
+ params: {
1212
+ callId,
1213
+ threadId,
1214
+ turnId,
1215
+ contentItems: [{ type: "input_text", text: output }],
1216
+ isError: false
1217
+ }
1218
+ });
1219
+ } catch (err) {
1220
+ const errMsg = err instanceof Error ? err.message : String(err);
1221
+ process.stderr.write(`[worker] tool "${toolName}" failed: ${errMsg}
1222
+ `);
1223
+ sendMessage({
1224
+ jsonrpc: "2.0",
1225
+ method: "dynamic_tool_call_response",
1226
+ params: {
1227
+ callId,
1228
+ threadId,
1229
+ turnId,
1230
+ contentItems: [{ type: "input_text", text: errMsg }],
1231
+ isError: true
1232
+ }
1233
+ });
1234
+ }
1235
+ }
1236
+ function handleServerMessage(msg) {
1237
+ if ("id" in msg && msg.id != null && ("result" in msg || "error" in msg)) {
1238
+ const id = String(msg.id);
1239
+ const pending = pendingRequests.get(id);
1240
+ if (pending) {
1241
+ pendingRequests.delete(id);
1242
+ if ("error" in msg) {
1243
+ pending.reject(new Error(JSON.stringify(msg.error)));
1244
+ } else {
1245
+ pending.resolve(msg.result);
1246
+ }
1247
+ }
1248
+ return;
1249
+ }
1250
+ runtimeState.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
1251
+ const orchestratorEventName = typeof msg.method === "string" ? msg.method : void 0;
1252
+ if (msg.method === "dynamic_tool_call_request" && msg.params != null) {
1253
+ const params = msg.params;
1254
+ void dispatchDynamicToolCall(params.callId, params.tool, params.threadId, params.turnId, params.arguments);
1255
+ emitOrchestratorChannelEvent(orchestratorEventName);
1256
+ return;
1257
+ }
1258
+ if (msg.method === "item/tool/requestUserInput") {
1259
+ process.stderr.write("[worker] user_input_required detected \u2014 terminating codex process\n");
1260
+ userInputRequired = true;
1261
+ runtimeState.status = "failed";
1262
+ if (runtimeState.run) {
1263
+ runtimeState.run.lastError = "turn_input_required: agent requires user input";
1264
+ }
1265
+ if (child.pid) {
1266
+ try {
1267
+ process.kill(child.pid, "SIGTERM");
1268
+ } catch {
1269
+ }
1270
+ }
1271
+ if (activeTurnTelemetry) {
1272
+ emitTurnFailedEvent(activeTurnTelemetry, runtimeState.run?.lastError ?? null);
1273
+ activeTurnTelemetry = null;
1274
+ }
1275
+ resolvePendingTurnCompletion();
1276
+ emitOrchestratorChannelEvent(orchestratorEventName);
1277
+ return;
1278
+ }
1279
+ if (msg.method === "turn/completed") {
1280
+ flushDeltaBuffer();
1281
+ const turnParams = msg.params ?? {};
1282
+ const turnUsage = extractAbsoluteTokenUsage(turnParams.usage);
1283
+ if (turnUsage) {
1284
+ applyTokenUsageUpdate("turn/completed", turnUsage);
1285
+ }
1286
+ const rateLimits2 = extractRateLimitPayload(turnParams);
1287
+ if (rateLimits2) {
1288
+ applyRateLimitUpdate("turn/completed", rateLimits2);
1289
+ }
1290
+ if (turnParams.inputRequired === true) {
1291
+ process.stderr.write("[worker] user_input_required detected \u2014 terminating codex process\n");
1292
+ userInputRequired = true;
1293
+ runtimeState.status = "failed";
1294
+ if (runtimeState.run) {
1295
+ runtimeState.run.lastError = "turn_input_required: agent requires user input";
1296
+ }
1297
+ if (child.pid) {
1298
+ try {
1299
+ process.kill(child.pid, "SIGTERM");
1300
+ } catch {
1301
+ }
1302
+ }
1303
+ if (activeTurnTelemetry) {
1304
+ emitTurnFailedEvent(activeTurnTelemetry, runtimeState.run?.lastError ?? null);
1305
+ activeTurnTelemetry = null;
1306
+ }
1307
+ resolvePendingTurnCompletion();
1308
+ emitOrchestratorChannelEvent(orchestratorEventName);
1309
+ return;
1310
+ }
1311
+ emitOrchestratorChannelEvent(orchestratorEventName);
1312
+ if (activeTurnTelemetry) {
1313
+ emitTurnCompletedEvent(activeTurnTelemetry);
1314
+ activeTurnTelemetry = null;
1315
+ }
1316
+ process.stderr.write("[worker] codex turn/completed\n");
1317
+ resolvePendingTurnCompletion();
1318
+ return;
1319
+ }
1320
+ if (msg.method === "turn/failed") {
1321
+ flushDeltaBuffer();
1322
+ const lastError = describeTurnTerminalEvent("turn/failed", msg.params ?? null);
1323
+ process.stderr.write(`[worker] codex turn/failed ${JSON.stringify(msg.params ?? {}).slice(0, 300)}
1324
+ `);
1325
+ markTurnTerminalFailure("failed", lastError);
1326
+ emitOrchestratorChannelEvent(orchestratorEventName);
1327
+ return;
1328
+ }
1329
+ if (msg.method === "turn/cancelled") {
1330
+ flushDeltaBuffer();
1331
+ const lastError = describeTurnTerminalEvent("turn/cancelled", msg.params ?? null);
1332
+ process.stderr.write(`[worker] codex turn/cancelled ${JSON.stringify(msg.params ?? {}).slice(0, 300)}
1333
+ `);
1334
+ markTurnTerminalFailure("canceled_by_reconciliation", lastError);
1335
+ emitOrchestratorChannelEvent(orchestratorEventName);
1336
+ return;
1337
+ }
1338
+ if (msg.method === "thread/tokenUsage/updated" || msg.method === "total_token_usage" || msg.method === "codex/event/token_count") {
1339
+ const tokenUsage = extractAbsoluteTokenUsage(msg.params);
1340
+ if (tokenUsage) {
1341
+ applyTokenUsageUpdate(msg.method, tokenUsage);
1342
+ }
1343
+ emitOrchestratorChannelEvent(orchestratorEventName);
1344
+ return;
1345
+ }
1346
+ const rateLimits = extractRateLimitPayload(msg.params);
1347
+ if (rateLimits && typeof msg.method === "string") {
1348
+ applyRateLimitUpdate(msg.method, rateLimits);
1349
+ }
1350
+ 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")) {
1351
+ const params = msg.params ?? {};
1352
+ const delta = typeof params.delta === "string" ? params.delta : "";
1353
+ const itemId = typeof params.item_id === "string" ? params.item_id : "";
1354
+ if (deltaBuffer?.itemId !== itemId) {
1355
+ flushDeltaBuffer();
1356
+ deltaBuffer = { itemId, text: delta };
1357
+ } else {
1358
+ deltaBuffer.text += delta;
1359
+ }
1360
+ emitOrchestratorChannelEvent(orchestratorEventName);
1361
+ return;
1362
+ }
1363
+ if (typeof msg.method === "string") {
1364
+ flushDeltaBuffer();
1365
+ emitOrchestratorChannelEvent(orchestratorEventName);
1366
+ process.stderr.write(`[worker] codex \u2192 ${msg.method} ${JSON.stringify(msg.params ?? {}).slice(0, 300)}
1367
+ `);
1368
+ }
1369
+ }
1370
+ child.stdout.on("data", (chunk) => {
1371
+ lineBuffer += chunk.toString("utf8");
1372
+ const lines = lineBuffer.split("\n");
1373
+ lineBuffer = lines.pop() ?? "";
1374
+ for (const line of lines) {
1375
+ const trimmed = line.trim();
1376
+ if (!trimmed)
1377
+ continue;
1378
+ try {
1379
+ const msg = JSON.parse(trimmed);
1380
+ handleServerMessage(msg);
1381
+ } catch {
1382
+ process.stderr.write(`[worker] codex stdout (non-JSON): ${trimmed}
1383
+ `);
1384
+ }
1385
+ }
1386
+ });
1387
+ try {
1388
+ process.stderr.write("[worker] sending codex initialize\n");
1389
+ await sendRequestWithTimeout("init-1", "initialize", {
1390
+ clientInfo: { name: "github-symphony", version: "0.1.0" },
1391
+ capabilities: {}
1392
+ });
1393
+ process.stderr.write("[worker] codex initialized\n");
1394
+ sendMessage({ jsonrpc: "2.0", method: "initialized", params: {} });
1395
+ const mcpServers = {};
1396
+ for (const t of plan.tools) {
1397
+ mcpServers[t.name] = {
1398
+ command: t.command,
1399
+ args: t.args,
1400
+ env: t.env
1401
+ };
1402
+ }
1403
+ if (remainingTurns <= 0) {
1404
+ process.stderr.write(`[worker] max_turns already exhausted by previous sessions (${cumulativeTurnCount}/${maxTurns})
1405
+ `);
1406
+ runtimeState.status = "completed";
1407
+ runtimeState.runPhase = "succeeded";
1408
+ runtimeState.sessionInfo.exitClassification = classifySessionExit({
1409
+ runPhase: runtimeState.runPhase,
1410
+ userInputRequired: false,
1411
+ budgetExceeded: false,
1412
+ convergenceDetected: false,
1413
+ maxTurnsReached: true
1414
+ });
1415
+ stopOrchestratorHeartbeatTimer();
1416
+ emitOrchestratorHeartbeat();
1417
+ await persistTokenUsageArtifact(env, runtimeState.tokenUsage);
1418
+ await waitForPendingOrchestratorChannelFlush(resolveTerminalOrchestratorChannelFlushTimeoutMs());
1419
+ setTimeout(() => {
1420
+ process.exit(0);
1421
+ }, 1500);
1422
+ return;
1423
+ }
1424
+ const baseThreadParams = {
1425
+ cwd: plan.cwd,
1426
+ developerInstructions: renderedPrompt,
1427
+ approvalPolicy,
1428
+ sandbox: threadSandbox,
1429
+ config: {
1430
+ mcp_servers: mcpServers
1431
+ }
1432
+ };
1433
+ const resumeThreadId = plan.resumeThreadId;
1434
+ let threadBootstrapMode = "fresh";
1435
+ process.stderr.write(`[worker] starting codex thread (mcp_servers: ${Object.keys(mcpServers).join(", ")})
1436
+ `);
1437
+ let threadResult;
1438
+ if (resumeThreadId) {
1439
+ process.stderr.write(`[worker] attempting thread/resume for ${resumeThreadId}
1440
+ `);
1441
+ try {
1442
+ threadResult = await sendRequestWithTimeout("thread-resume-1", "thread/resume", {
1443
+ ...baseThreadParams,
1444
+ threadId: resumeThreadId
1445
+ });
1446
+ threadBootstrapMode = "resume";
1447
+ } catch (error) {
1448
+ const message = error instanceof Error ? error.message : String(error ?? "unknown");
1449
+ threadBootstrapMode = "soft-resume";
1450
+ process.stderr.write(`[worker] thread/resume failed for ${resumeThreadId}: ${message}; falling back to thread/start
1451
+ `);
1452
+ threadResult = await sendRequestWithTimeout("thread-1", "thread/start", {
1453
+ ...baseThreadParams,
1454
+ ephemeral: false
1455
+ });
1456
+ }
1457
+ } else {
1458
+ threadResult = await sendRequestWithTimeout("thread-1", "thread/start", {
1459
+ ...baseThreadParams,
1460
+ ephemeral: false
1461
+ });
1462
+ }
1463
+ const threadId = threadResult.thread_id ?? threadResult.thread?.id;
1464
+ runtimeState.sessionInfo.threadId = threadId ?? null;
1465
+ runtimeState.sessionInfo.turnId = null;
1466
+ runtimeState.sessionInfo.sessionId = null;
1467
+ runtimeState.sessionInfo.exitClassification = null;
1468
+ runtimeState.sessionId = null;
1469
+ process.stderr.write(`[worker] codex thread started (id=${String(threadId ?? "unknown")})
1470
+ `);
1471
+ if (!threadId) {
1472
+ process.stderr.write("[worker] warning: no threadId returned; cannot start turn\n");
1473
+ return;
1474
+ }
1475
+ let turnCount = 0;
1476
+ let requestIdCounter = 0;
1477
+ let maxTurnsReached = false;
1478
+ for (let turn = 0; turn < remainingTurns; turn++) {
1479
+ budgetExceededReason = checkSessionBudgets(turn);
1480
+ if (budgetExceededReason) {
1481
+ process.stderr.write(`[worker] session budget exceeded (${budgetExceededReason}) \u2014 exiting
1482
+ `);
1483
+ break;
1484
+ }
1485
+ turnCount = turn + 1;
1486
+ const globalTurnCount = cumulativeTurnCount + turnCount;
1487
+ runtimeState.sessionInfo.turnCount = turnCount;
1488
+ runtimeState.runPhase = "streaming_turn";
1489
+ const isFirstTurn = turn === 0;
1490
+ const turnInput = isFirstTurn ? buildInitialTurnInput({
1491
+ renderedPrompt,
1492
+ mode: threadBootstrapMode,
1493
+ lastTurnSummary,
1494
+ cumulativeTurnCount,
1495
+ continuationGuidance
1496
+ }) : buildContinuationTurnInput({
1497
+ continuationGuidance,
1498
+ lastTurnSummary,
1499
+ cumulativeTurnCount: globalTurnCount - 1
1500
+ });
1501
+ process.stderr.write(`[worker] starting codex turn ${globalTurnCount}/${maxTurns}${isFirstTurn ? " (initial)" : " (continuation)"}
1502
+ `);
1503
+ requestIdCounter += 1;
1504
+ const turnRequestId = `turn-${requestIdCounter}`;
1505
+ const turnResult = await sendRequestWithTimeout(turnRequestId, "turn/start", {
1506
+ threadId,
1507
+ input: [{ type: "text", text: turnInput }],
1508
+ cwd: plan.cwd,
1509
+ title: composeTurnTitle(issueIdentifier, env.SYMPHONY_ISSUE_TITLE),
1510
+ approvalPolicy,
1511
+ sandboxPolicy: turnSandboxPolicy
1512
+ });
1513
+ const turnId = turnResult.turn_id ?? turnResult.turn?.id;
1514
+ const sessionId = threadId && turnId ? `${threadId}-${turnId}` : null;
1515
+ runtimeState.sessionInfo.turnId = turnId ?? null;
1516
+ runtimeState.sessionInfo.sessionId = sessionId;
1517
+ runtimeState.sessionId = sessionId;
1518
+ activeTurnTelemetry = {
1519
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1520
+ threadId: threadId ?? null,
1521
+ turnId: turnId ?? null,
1522
+ turnCount,
1523
+ sessionId,
1524
+ tokenUsageBaseline: cloneTokenUsageSnapshot()
1525
+ };
1526
+ process.stderr.write(`[worker] codex turn started (id=${String(turnId ?? "unknown")})
1527
+ `);
1528
+ process.stderr.write(`[worker] session_id=${String(sessionId ?? "unknown")}
1529
+ `);
1530
+ emitTurnStartedEvent(activeTurnTelemetry);
1531
+ await waitForTurnWithTimeout();
1532
+ if (userInputRequired) {
1533
+ process.stderr.write("[worker] exiting due to user_input_required\n");
1534
+ break;
1535
+ }
1536
+ if (turnTerminalFailurePhase) {
1537
+ process.stderr.write(`[worker] exiting due to ${turnTerminalFailurePhase}
1538
+ `);
1539
+ break;
1540
+ }
1541
+ budgetExceededReason = checkSessionBudgets(turnCount);
1542
+ if (budgetExceededReason) {
1543
+ process.stderr.write(`[worker] session budget exceeded (${budgetExceededReason}) \u2014 exiting
1544
+ `);
1545
+ break;
1546
+ }
1547
+ if (turn + 1 >= remainingTurns) {
1548
+ maxTurnsReached = true;
1549
+ process.stderr.write(`[worker] max_turns (${maxTurns}) reached across sessions \u2014 exiting
1550
+ `);
1551
+ break;
1552
+ }
1553
+ const trackerState = await refreshTrackerState(env);
1554
+ process.stderr.write(`[worker] tracker state refresh: ${trackerState}
1555
+ `);
1556
+ if (trackerState === "non-actionable") {
1557
+ runtimeState.runPhase = "finishing";
1558
+ runtimeState.executionPhase = resolveFinalExecutionPhase({
1559
+ currentPhase: runtimeState.executionPhase,
1560
+ trackerState,
1561
+ userInputRequired: false
1562
+ });
1563
+ process.stderr.write("[worker] issue no longer actionable \u2014 exiting multi-turn loop\n");
1564
+ break;
1565
+ }
1566
+ const currentTurnProgressSnapshot = {
1567
+ ...captureTurnWorkspaceSnapshot(plan.cwd),
1568
+ lastError: runtimeState.run?.lastError ?? null
1569
+ };
1570
+ const turnProgress = evaluateTurnProgress(previousTurnProgressSnapshot, currentTurnProgressSnapshot);
1571
+ previousTurnProgressSnapshot = currentTurnProgressSnapshot;
1572
+ if (turnProgress.nonProductive) {
1573
+ consecutiveNonProductiveTurns += 1;
1574
+ process.stderr.write(`[worker] non-productive turn detected (${consecutiveNonProductiveTurns}/${maxNonProductiveTurns})${turnProgress.reason ? `: ${turnProgress.reason}` : ""}
1575
+ `);
1576
+ } else {
1577
+ consecutiveNonProductiveTurns = 0;
1578
+ }
1579
+ if (consecutiveNonProductiveTurns >= maxNonProductiveTurns) {
1580
+ convergenceDetected = true;
1581
+ if (runtimeState.run) {
1582
+ runtimeState.run.lastError = turnProgress.reason ? `convergence_detected: ${turnProgress.reason}` : "convergence_detected: repeated non-productive turn results";
1583
+ }
1584
+ process.stderr.write(`[worker] convergence detected after ${consecutiveNonProductiveTurns} non-productive turns \u2014 exiting
1585
+ `);
1586
+ break;
1587
+ }
1588
+ }
1589
+ process.stderr.write(`[worker] multi-turn loop complete after ${turnCount} turn(s) \u2014 exiting worker
1590
+ `);
1591
+ runtimeState.runPhase = "finishing";
1592
+ runtimeState.status = userInputRequired || turnTerminalFailurePhase ? "failed" : "completed";
1593
+ runtimeState.runPhase = convergenceDetected ? "failed" : userInputRequired ? "failed" : turnTerminalFailurePhase ?? "succeeded";
1594
+ runtimeState.sessionInfo.exitClassification = classifySessionExit({
1595
+ runPhase: runtimeState.runPhase,
1596
+ userInputRequired,
1597
+ budgetExceeded: budgetExceededReason !== null,
1598
+ convergenceDetected,
1599
+ maxTurnsReached
1600
+ });
1601
+ stopOrchestratorHeartbeatTimer();
1602
+ emitOrchestratorHeartbeat();
1603
+ await persistTokenUsageArtifact(env, runtimeState.tokenUsage);
1604
+ await waitForPendingOrchestratorChannelFlush(resolveTerminalOrchestratorChannelFlushTimeoutMs());
1605
+ setTimeout(() => {
1606
+ process.exit(userInputRequired || turnTerminalFailurePhase ? 1 : 0);
1607
+ }, 1500);
1608
+ } catch (err) {
1609
+ const errMsg = err instanceof Error ? err.message : String(err);
1610
+ process.stderr.write(`[worker] codex client protocol error: ${errMsg}
1611
+ `);
1612
+ runtimeState.status = "failed";
1613
+ runtimeState.runPhase = "failed";
1614
+ if (runtimeState.run) {
1615
+ runtimeState.run.lastError = `Codex client protocol error: ${errMsg}`;
1616
+ }
1617
+ if (errMsg.startsWith("response_timeout:")) {
1618
+ runtimeState.runPhase = "stalled";
1619
+ if (runtimeState.run) {
1620
+ runtimeState.run.lastError = errMsg;
1621
+ }
1622
+ } else if (errMsg.startsWith("turn_timeout:")) {
1623
+ runtimeState.runPhase = "timed_out";
1624
+ if (runtimeState.run) {
1625
+ runtimeState.run.lastError = errMsg;
1626
+ }
1627
+ }
1628
+ runtimeState.sessionInfo.exitClassification = classifySessionExit({
1629
+ runPhase: runtimeState.runPhase,
1630
+ userInputRequired: false,
1631
+ budgetExceeded: false,
1632
+ convergenceDetected: false,
1633
+ maxTurnsReached: false
1634
+ });
1635
+ if (activeTurnTelemetry) {
1636
+ emitTurnFailedEvent(activeTurnTelemetry, runtimeState.run?.lastError ?? errMsg);
1637
+ activeTurnTelemetry = null;
1638
+ }
1639
+ stopOrchestratorHeartbeatTimer();
1640
+ emitOrchestratorHeartbeat();
1641
+ await persistTokenUsageArtifact(env, runtimeState.tokenUsage);
1642
+ await waitForPendingOrchestratorChannelFlush(resolveTerminalOrchestratorChannelFlushTimeoutMs());
1643
+ setTimeout(() => {
1644
+ process.exit(1);
1645
+ }, 1500);
1646
+ }
1647
+ }
1648
+ function applyTokenUsageUpdate(source, tokenUsage) {
1649
+ runtimeState.tokenUsage.inputTokens = tokenUsage.inputTokens;
1650
+ runtimeState.tokenUsage.outputTokens = tokenUsage.outputTokens;
1651
+ runtimeState.tokenUsage.totalTokens = tokenUsage.totalTokens;
1652
+ process.stderr.write(`[worker] token_usage source=${source} input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens}
1653
+ `);
1654
+ }
1655
+ function applyRateLimitUpdate(source, rateLimits) {
1656
+ runtimeState.rateLimits = {
1657
+ ...rateLimits,
1658
+ source: "codex"
1659
+ };
1660
+ process.stderr.write(`[worker] rate_limits source=${source} payload=${JSON.stringify(runtimeState.rateLimits).slice(0, 300)}
1661
+ `);
1662
+ }
1663
+ function extractRateLimitPayload(value) {
1664
+ if (!value || typeof value !== "object") {
1665
+ return null;
1666
+ }
1667
+ const direct = parseRateLimitRecord(value);
1668
+ if (direct) {
1669
+ return direct;
1670
+ }
1671
+ const record = value;
1672
+ const preferredKeys = [
1673
+ "rate_limits",
1674
+ "rateLimits",
1675
+ "rate_limit",
1676
+ "rateLimit",
1677
+ "info",
1678
+ "msg",
1679
+ "event",
1680
+ "data",
1681
+ "result",
1682
+ "payload"
1683
+ ];
1684
+ for (const key of preferredKeys) {
1685
+ if (key in record) {
1686
+ const nested = extractRateLimitPayload(record[key]);
1687
+ if (nested) {
1688
+ return nested;
1689
+ }
1690
+ }
1691
+ }
1692
+ for (const nestedValue of Object.values(record)) {
1693
+ const nested = extractRateLimitPayload(nestedValue);
1694
+ if (nested) {
1695
+ return nested;
1696
+ }
1697
+ }
1698
+ return null;
1699
+ }
1700
+ function parseRateLimitRecord(value) {
1701
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1702
+ return null;
1703
+ }
1704
+ const record = value;
1705
+ const keys = Object.keys(record);
1706
+ const directKeys = /* @__PURE__ */ new Set([
1707
+ "limit",
1708
+ "remaining",
1709
+ "used",
1710
+ "reset",
1711
+ "resetAt",
1712
+ "resets_at",
1713
+ "reset_at",
1714
+ "window_minutes",
1715
+ "resource",
1716
+ "retry_after"
1717
+ ]);
1718
+ if (!keys.some((key) => directKeys.has(key))) {
1719
+ return null;
1720
+ }
1721
+ return { ...record };
1722
+ }
1723
+ function extractAbsoluteTokenUsage(value) {
1724
+ const direct = parseTokenUsageSnapshot(value);
1725
+ if (direct) {
1726
+ return direct;
1727
+ }
1728
+ if (!value || typeof value !== "object") {
1729
+ return null;
1730
+ }
1731
+ const record = value;
1732
+ const preferredKeys = [
1733
+ "total_token_usage",
1734
+ "token_usage",
1735
+ "info",
1736
+ "msg",
1737
+ "event",
1738
+ "data",
1739
+ "result",
1740
+ "payload"
1741
+ ];
1742
+ for (const key of preferredKeys) {
1743
+ if (key in record) {
1744
+ const nested = extractAbsoluteTokenUsage(record[key]);
1745
+ if (nested) {
1746
+ return nested;
1747
+ }
1748
+ }
1749
+ }
1750
+ for (const [key, nestedValue] of Object.entries(record)) {
1751
+ if (key === "last_token_usage") {
1752
+ continue;
1753
+ }
1754
+ const nested = extractAbsoluteTokenUsage(nestedValue);
1755
+ if (nested) {
1756
+ return nested;
1757
+ }
1758
+ }
1759
+ return null;
1760
+ }
1761
+ function parseTokenUsageSnapshot(value) {
1762
+ if (!value || typeof value !== "object") {
1763
+ return null;
1764
+ }
1765
+ const record = value;
1766
+ const inputTokens = typeof record.input_tokens === "number" ? record.input_tokens : typeof record.inputTokens === "number" ? record.inputTokens : null;
1767
+ const outputTokens = typeof record.output_tokens === "number" ? record.output_tokens : typeof record.outputTokens === "number" ? record.outputTokens : null;
1768
+ const explicitTotalTokens = typeof record.total_tokens === "number" ? record.total_tokens : typeof record.totalTokens === "number" ? record.totalTokens : null;
1769
+ if (inputTokens === null && outputTokens === null && explicitTotalTokens === null) {
1770
+ return null;
1771
+ }
1772
+ const normalizedInputTokens = inputTokens ?? 0;
1773
+ const normalizedOutputTokens = outputTokens ?? 0;
1774
+ const normalizedTotalTokens = explicitTotalTokens ?? normalizedInputTokens + normalizedOutputTokens;
1775
+ if (normalizedInputTokens <= 0 && normalizedOutputTokens <= 0 && normalizedTotalTokens <= 0) {
1776
+ return null;
1777
+ }
1778
+ return {
1779
+ inputTokens: normalizedInputTokens,
1780
+ outputTokens: normalizedOutputTokens,
1781
+ totalTokens: normalizedTotalTokens || normalizedInputTokens + normalizedOutputTokens
1782
+ };
1783
+ }
1784
+ async function refreshTrackerState(env) {
1785
+ const orchestratorUrl = env.SYMPHONY_ORCHESTRATOR_URL;
1786
+ const issueIdentifier = env.SYMPHONY_ISSUE_IDENTIFIER;
1787
+ if (!orchestratorUrl) {
1788
+ return "unknown";
1789
+ }
1790
+ try {
1791
+ const response = await fetch(`${orchestratorUrl}/api/v1/state`);
1792
+ if (!response.ok)
1793
+ return "unknown";
1794
+ const status = await response.json();
1795
+ const isActive = status.activeRuns?.some((run) => run.issueIdentifier === issueIdentifier);
1796
+ return isActive ? "active" : "non-actionable";
1797
+ } catch {
1798
+ return "unknown";
1799
+ }
1800
+ }
1801
+ function runToolProcess(toolDef, inputJson) {
1802
+ return new Promise((resolve2, reject) => {
1803
+ const toolEnv = {
1804
+ ...process.env,
1805
+ ...toolDef.env
1806
+ };
1807
+ const toolProc = spawn2(toolDef.command, toolDef.args, {
1808
+ env: toolEnv,
1809
+ stdio: "pipe"
1810
+ });
1811
+ const stdout = [];
1812
+ const stderr = [];
1813
+ toolProc.stdout?.on("data", (chunk) => stdout.push(chunk));
1814
+ toolProc.stderr?.on("data", (chunk) => stderr.push(chunk));
1815
+ toolProc.once("error", (err) => reject(err));
1816
+ toolProc.once("exit", (code) => {
1817
+ const output = Buffer.concat(stdout).toString("utf8").trim();
1818
+ if (code === 0) {
1819
+ resolve2(output || "{}");
1820
+ } else {
1821
+ const errOutput = Buffer.concat(stderr).toString("utf8").trim();
1822
+ reject(new Error(`Tool exited with code ${code ?? "unknown"}: ${errOutput || output}`));
1823
+ }
1824
+ });
1825
+ toolProc.stdin?.write(inputJson);
1826
+ toolProc.stdin?.end();
1827
+ });
1828
+ }