@gh-symphony/cli 0.0.15 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-RNWX7DQU.js → chunk-EFMFGOWM.js} +123 -1165
- package/dist/{chunk-M7OSMUTN.js → chunk-MHIWAIVD.js} +5 -3
- package/dist/chunk-TF3QNWNC.js +1121 -0
- package/dist/index.js +4 -4
- package/dist/{project-3ELXQ35D.js → project-557FE2GD.js} +3 -2
- package/dist/{recover-T6ME6C56.js → recover-LVBI2TGH.js} +2 -1
- package/dist/{run-DYINRZHK.js → run-WITYAYFZ.js} +2 -1
- package/dist/{start-PIFQMIC2.js → start-JUFKNL3N.js} +3 -2
- package/dist/worker-entry.js +1828 -0
- package/package.json +4 -4
|
@@ -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
|
+
}
|