@gh-symphony/cli 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -2
- package/dist/chunk-A67CMOYE.js +684 -0
- package/dist/{chunk-TILHWBP6.js → chunk-C67H3OUL.js} +239 -36
- package/dist/{chunk-RN2PACNV.js → chunk-JN3TQVFV.js} +721 -74
- package/dist/{chunk-GKENCODJ.js → chunk-KY6WKH66.js} +437 -101
- package/dist/{chunk-6CI3UUMH.js → chunk-MYVJ6HK4.js} +950 -1240
- package/dist/{chunk-M3IFVLQS.js → chunk-QEONJ5DZ.js} +978 -72
- package/dist/{chunk-H2YXSYOZ.js → chunk-S6VIK4FF.js} +59 -31
- package/dist/chunk-SXGT7LOF.js +1060 -0
- package/dist/{doctor-IYHCFXOZ.js → doctor-4HBRICHP.js} +102 -37
- package/dist/index.js +38 -17
- package/dist/{init-KZT6YNOH.js → init-HZ3JEDGQ.js} +7 -2
- package/dist/{project-DNALEWO3.js → project-25NQ4J4Y.js} +8 -6
- package/dist/{recover-C3V2QAUB.js → recover-L3MJHHDA.js} +4 -2
- package/dist/{repo-HDDE7OUI.js → repo-TDCWQR6P.js} +72 -14
- package/dist/{run-XI2S5Y4V.js → run-XJQ6BF7U.js} +4 -2
- package/dist/{setup-K4CYYJBF.js → setup-B2SVLW2R.js} +46 -8
- package/dist/{start-M6IQGRFO.js → start-I2CC7BLW.js} +6 -4
- package/dist/{upgrade-F4VE4XBS.js → upgrade-OJXPZRYE.js} +2 -2
- package/dist/{version-Y5RYNWMF.js → version-TBDCTKDO.js} +1 -1
- package/dist/worker-entry.js +522 -867
- package/dist/{workflow-TBIFY5MO.js → workflow-BLJH2HC3.js} +176 -10
- package/package.json +5 -3
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveTrackerAdapter
|
|
4
|
+
} from "./chunk-SXGT7LOF.js";
|
|
2
5
|
import {
|
|
3
6
|
DEFAULT_MAX_FAILURE_RETRIES,
|
|
4
7
|
DEFAULT_WORKFLOW_LIFECYCLE,
|
|
@@ -24,11 +27,13 @@ import {
|
|
|
24
27
|
readJsonFile,
|
|
25
28
|
renderPrompt,
|
|
26
29
|
resolveIssueWorkspaceDirectory,
|
|
30
|
+
resolveWorkflowRuntimeCommand,
|
|
31
|
+
resolveWorkflowRuntimeTimeouts,
|
|
27
32
|
safeReadDir,
|
|
28
33
|
scheduleRetryAt
|
|
29
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-QEONJ5DZ.js";
|
|
30
35
|
|
|
31
|
-
// ../orchestrator/
|
|
36
|
+
// ../orchestrator/src/service.ts
|
|
32
37
|
import { mkdir as mkdir3, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
33
38
|
import { createWriteStream, mkdirSync } from "fs";
|
|
34
39
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -36,10 +41,18 @@ import { join as join3 } from "path";
|
|
|
36
41
|
import { StringDecoder } from "string_decoder";
|
|
37
42
|
import { fileURLToPath } from "url";
|
|
38
43
|
|
|
39
|
-
// ../orchestrator/
|
|
44
|
+
// ../orchestrator/src/git.ts
|
|
40
45
|
import { spawn } from "child_process";
|
|
41
46
|
import { randomUUID } from "crypto";
|
|
42
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
access,
|
|
49
|
+
mkdir,
|
|
50
|
+
readFile,
|
|
51
|
+
rename,
|
|
52
|
+
rm,
|
|
53
|
+
stat,
|
|
54
|
+
writeFile
|
|
55
|
+
} from "fs/promises";
|
|
43
56
|
import { constants } from "fs";
|
|
44
57
|
import { join } from "path";
|
|
45
58
|
var workflowConfigStore = new WorkflowConfigStore();
|
|
@@ -81,7 +94,10 @@ async function syncRepositoryForRun(input) {
|
|
|
81
94
|
} else {
|
|
82
95
|
await rm(repositoryDirectory, { recursive: true, force: true });
|
|
83
96
|
}
|
|
84
|
-
const tempRepositoryDirectory = join(
|
|
97
|
+
const tempRepositoryDirectory = join(
|
|
98
|
+
input.targetDirectory,
|
|
99
|
+
`repository.tmp-${process.pid}-${Date.now()}`
|
|
100
|
+
);
|
|
85
101
|
await rm(tempRepositoryDirectory, { recursive: true, force: true });
|
|
86
102
|
try {
|
|
87
103
|
await runCommand("git", [
|
|
@@ -115,7 +131,10 @@ async function loadRepositoryWorkflow(repositoryDirectory, _repository) {
|
|
|
115
131
|
if (isMissingFileError(error)) {
|
|
116
132
|
return createDefaultWorkflowResolution();
|
|
117
133
|
}
|
|
118
|
-
return createInvalidWorkflowResolution(
|
|
134
|
+
return createInvalidWorkflowResolution(
|
|
135
|
+
workflowPath,
|
|
136
|
+
error instanceof Error ? error.message : "workflow_parse_error"
|
|
137
|
+
);
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
140
|
function runCommand(command, args) {
|
|
@@ -133,7 +152,11 @@ function runCommand(command, args) {
|
|
|
133
152
|
resolve4();
|
|
134
153
|
return;
|
|
135
154
|
}
|
|
136
|
-
reject(
|
|
155
|
+
reject(
|
|
156
|
+
new Error(
|
|
157
|
+
stderr.trim() || `${command} exited with code ${code ?? "unknown"}`
|
|
158
|
+
)
|
|
159
|
+
);
|
|
137
160
|
});
|
|
138
161
|
});
|
|
139
162
|
}
|
|
@@ -168,7 +191,11 @@ function runCommandCapture(command, args) {
|
|
|
168
191
|
resolve4(stdout.trim());
|
|
169
192
|
return;
|
|
170
193
|
}
|
|
171
|
-
reject(
|
|
194
|
+
reject(
|
|
195
|
+
new Error(
|
|
196
|
+
stderr.trim() || `${command} exited with code ${code ?? "unknown"}`
|
|
197
|
+
)
|
|
198
|
+
);
|
|
172
199
|
});
|
|
173
200
|
});
|
|
174
201
|
}
|
|
@@ -186,9 +213,13 @@ async function acquireRepositoryLock(lockDirectory) {
|
|
|
186
213
|
for (; ; ) {
|
|
187
214
|
try {
|
|
188
215
|
await mkdir(lockDirectory);
|
|
189
|
-
await writeFile(
|
|
216
|
+
await writeFile(
|
|
217
|
+
join(lockDirectory, "owner"),
|
|
218
|
+
`${ownerToken}
|
|
190
219
|
${(/* @__PURE__ */ new Date()).toISOString()}
|
|
191
|
-
`,
|
|
220
|
+
`,
|
|
221
|
+
"utf8"
|
|
222
|
+
);
|
|
192
223
|
return ownerToken;
|
|
193
224
|
} catch (error) {
|
|
194
225
|
if (!isAlreadyExistsError(error)) {
|
|
@@ -201,7 +232,9 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
|
201
232
|
continue;
|
|
202
233
|
}
|
|
203
234
|
if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
|
|
204
|
-
throw new Error(
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Timed out waiting for repository cache lock: ${lockDirectory}`
|
|
237
|
+
);
|
|
205
238
|
}
|
|
206
239
|
await wait(LOCK_RETRY_MS);
|
|
207
240
|
}
|
|
@@ -232,7 +265,9 @@ async function isStaleLock(lockDirectory) {
|
|
|
232
265
|
}
|
|
233
266
|
}
|
|
234
267
|
function isAlreadyExistsError(error) {
|
|
235
|
-
return Boolean(
|
|
268
|
+
return Boolean(
|
|
269
|
+
error && typeof error === "object" && "code" in error && error.code === "EEXIST"
|
|
270
|
+
);
|
|
236
271
|
}
|
|
237
272
|
async function readLockOwner(lockDirectory) {
|
|
238
273
|
await access(join(lockDirectory, "owner"), constants.R_OK);
|
|
@@ -245,21 +280,30 @@ function wait(ms) {
|
|
|
245
280
|
});
|
|
246
281
|
}
|
|
247
282
|
function isMissingFileError(error) {
|
|
248
|
-
return Boolean(
|
|
283
|
+
return Boolean(
|
|
284
|
+
error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
285
|
+
);
|
|
249
286
|
}
|
|
250
287
|
|
|
251
|
-
// ../orchestrator/
|
|
252
|
-
import {
|
|
288
|
+
// ../orchestrator/src/fs-store.ts
|
|
289
|
+
import {
|
|
290
|
+
mkdir as mkdir2,
|
|
291
|
+
open,
|
|
292
|
+
rename as rename2,
|
|
293
|
+
rm as rm2,
|
|
294
|
+
stat as stat2,
|
|
295
|
+
writeFile as writeFile2,
|
|
296
|
+
appendFile
|
|
297
|
+
} from "fs/promises";
|
|
253
298
|
import { dirname, join as join2, relative, resolve } from "path";
|
|
254
299
|
var OrchestratorFsStore = class {
|
|
255
|
-
runtimeRoot;
|
|
256
|
-
resolvedRuntimeRoot;
|
|
257
|
-
resolvedEventsMirrorRoot;
|
|
258
300
|
constructor(runtimeRoot, options = {}) {
|
|
259
301
|
this.runtimeRoot = runtimeRoot;
|
|
260
302
|
this.resolvedRuntimeRoot = resolve(runtimeRoot);
|
|
261
303
|
this.resolvedEventsMirrorRoot = options.eventsMirrorRoot ? resolve(options.eventsMirrorRoot) : null;
|
|
262
304
|
}
|
|
305
|
+
resolvedRuntimeRoot;
|
|
306
|
+
resolvedEventsMirrorRoot;
|
|
263
307
|
projectsRoot() {
|
|
264
308
|
return join2(this.runtimeRoot, "projects");
|
|
265
309
|
}
|
|
@@ -276,10 +320,15 @@ var OrchestratorFsStore = class {
|
|
|
276
320
|
return join2(this.projectRunsDir(projectId), runId);
|
|
277
321
|
}
|
|
278
322
|
async loadProjectConfig(projectId) {
|
|
279
|
-
return readJsonFile(
|
|
323
|
+
return readJsonFile(
|
|
324
|
+
join2(this.projectDir(projectId), "project.json")
|
|
325
|
+
);
|
|
280
326
|
}
|
|
281
327
|
async saveProjectConfig(config) {
|
|
282
|
-
await writeJsonFile(
|
|
328
|
+
await writeJsonFile(
|
|
329
|
+
join2(this.projectDir(config.projectId), "project.json"),
|
|
330
|
+
config
|
|
331
|
+
);
|
|
283
332
|
}
|
|
284
333
|
async loadProjectIssueOrchestrations(projectId) {
|
|
285
334
|
const issuesPath = join2(this.projectDir(projectId), "issues.json");
|
|
@@ -295,53 +344,78 @@ var OrchestratorFsStore = class {
|
|
|
295
344
|
if (legacyLeases.length === 0) {
|
|
296
345
|
return [];
|
|
297
346
|
}
|
|
298
|
-
const migratedIssues = legacyLeases.map(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
347
|
+
const migratedIssues = legacyLeases.map(
|
|
348
|
+
(lease) => ({
|
|
349
|
+
issueId: lease.issueId,
|
|
350
|
+
identifier: lease.issueIdentifier,
|
|
351
|
+
workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(
|
|
352
|
+
lease.issueIdentifier
|
|
353
|
+
),
|
|
354
|
+
completedOnce: false,
|
|
355
|
+
failureRetryCount: 0,
|
|
356
|
+
state: lease.status === "active" ? "claimed" : "released",
|
|
357
|
+
currentRunId: lease.status === "active" ? lease.runId : null,
|
|
358
|
+
retryEntry: null,
|
|
359
|
+
updatedAt: lease.updatedAt
|
|
360
|
+
})
|
|
361
|
+
);
|
|
309
362
|
await this.saveProjectIssueOrchestrations(projectId, migratedIssues);
|
|
310
363
|
return migratedIssues;
|
|
311
364
|
}
|
|
312
365
|
async saveProjectIssueOrchestrations(projectId, issues) {
|
|
313
|
-
await writeJsonFile(
|
|
366
|
+
await writeJsonFile(
|
|
367
|
+
join2(this.projectDir(projectId), "issues.json"),
|
|
368
|
+
issues
|
|
369
|
+
);
|
|
314
370
|
}
|
|
315
371
|
async saveProjectStatus(status) {
|
|
316
|
-
await writeJsonFile(
|
|
372
|
+
await writeJsonFile(
|
|
373
|
+
join2(this.projectDir(status.projectId), "status.json"),
|
|
374
|
+
status
|
|
375
|
+
);
|
|
317
376
|
}
|
|
318
377
|
async loadProjectStatus(projectId) {
|
|
319
|
-
return await readJsonFile(
|
|
378
|
+
return await readJsonFile(
|
|
379
|
+
join2(this.projectDir(projectId), "status.json")
|
|
380
|
+
) ?? null;
|
|
320
381
|
}
|
|
321
382
|
async loadRun(runId, projectId) {
|
|
322
383
|
const runDirectory = projectId !== void 0 ? this.runDir(runId, projectId) : await this.findRunDir(runId);
|
|
323
384
|
if (!runDirectory) {
|
|
324
385
|
return null;
|
|
325
386
|
}
|
|
326
|
-
return await readJsonFile(
|
|
387
|
+
return await readJsonFile(
|
|
388
|
+
join2(runDirectory, "run.json")
|
|
389
|
+
) ?? null;
|
|
327
390
|
}
|
|
328
391
|
async loadAllRuns() {
|
|
329
392
|
const projectIds = await safeReadDir(this.projectsRoot());
|
|
330
|
-
const runDirectories = await Promise.all(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
393
|
+
const runDirectories = await Promise.all(
|
|
394
|
+
projectIds.map(async (projectId) => {
|
|
395
|
+
const entries = await safeReadDir(this.projectRunsDir(projectId));
|
|
396
|
+
return entries.map((entry) => this.runDir(entry, projectId));
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
const runs = await Promise.all(
|
|
400
|
+
runDirectories.flat().map(
|
|
401
|
+
(directory) => readJsonFile(join2(directory, "run.json"))
|
|
402
|
+
)
|
|
403
|
+
);
|
|
335
404
|
return runs.filter((run) => Boolean(run));
|
|
336
405
|
}
|
|
337
406
|
async saveRun(run) {
|
|
338
|
-
await writeJsonFile(
|
|
407
|
+
await writeJsonFile(
|
|
408
|
+
join2(this.runDir(run.runId, run.projectId), "run.json"),
|
|
409
|
+
run
|
|
410
|
+
);
|
|
339
411
|
}
|
|
340
412
|
async appendRunEvent(runId, event) {
|
|
341
413
|
const resolvedProjectId = "projectId" in event && typeof event.projectId === "string" ? event.projectId : void 0;
|
|
342
414
|
const runDirectory = resolvedProjectId !== void 0 ? this.runDir(runId, resolvedProjectId) : await this.findRunDir(runId);
|
|
343
415
|
if (!runDirectory) {
|
|
344
|
-
throw new Error(
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Unable to resolve run directory for event append: ${runId}`
|
|
418
|
+
);
|
|
345
419
|
}
|
|
346
420
|
const path = join2(runDirectory, "events.ndjson");
|
|
347
421
|
const resolvedPath = resolve(path);
|
|
@@ -362,7 +436,9 @@ var OrchestratorFsStore = class {
|
|
|
362
436
|
mode: 420
|
|
363
437
|
});
|
|
364
438
|
} catch (error) {
|
|
365
|
-
console.warn(
|
|
439
|
+
console.warn(
|
|
440
|
+
`Failed to mirror orchestrator event log to ${mirrorPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
441
|
+
);
|
|
366
442
|
}
|
|
367
443
|
}
|
|
368
444
|
async loadRecentRunEvents(runId, limit = 20, projectId) {
|
|
@@ -410,16 +486,28 @@ var OrchestratorFsStore = class {
|
|
|
410
486
|
return join2(this.projectDir(projectId), "issues", workspaceKey);
|
|
411
487
|
}
|
|
412
488
|
async loadIssueWorkspace(projectId, workspaceKey) {
|
|
413
|
-
return await readJsonFile(
|
|
489
|
+
return await readJsonFile(
|
|
490
|
+
join2(this.issueWorkspaceDir(projectId, workspaceKey), "workspace.json")
|
|
491
|
+
) ?? null;
|
|
414
492
|
}
|
|
415
493
|
async loadIssueWorkspaces(projectId) {
|
|
416
494
|
const issuesDir = join2(this.projectDir(projectId), "issues");
|
|
417
495
|
const entries = await safeReadDir(issuesDir);
|
|
418
|
-
const records = await Promise.all(
|
|
419
|
-
|
|
496
|
+
const records = await Promise.all(
|
|
497
|
+
entries.map((entry) => this.loadIssueWorkspace(projectId, entry))
|
|
498
|
+
);
|
|
499
|
+
return records.filter(
|
|
500
|
+
(record) => Boolean(record)
|
|
501
|
+
);
|
|
420
502
|
}
|
|
421
503
|
async saveIssueWorkspace(record) {
|
|
422
|
-
await writeJsonFile(
|
|
504
|
+
await writeJsonFile(
|
|
505
|
+
join2(
|
|
506
|
+
this.issueWorkspaceDir(record.projectId, record.workspaceKey),
|
|
507
|
+
"workspace.json"
|
|
508
|
+
),
|
|
509
|
+
record
|
|
510
|
+
);
|
|
423
511
|
}
|
|
424
512
|
async removeIssueWorkspace(projectId, workspaceKey) {
|
|
425
513
|
const dir = this.issueWorkspaceDir(projectId, workspaceKey);
|
|
@@ -429,7 +517,9 @@ var OrchestratorFsStore = class {
|
|
|
429
517
|
const projectIds = await safeReadDir(this.projectsRoot());
|
|
430
518
|
for (const projectId of projectIds) {
|
|
431
519
|
const candidate = this.runDir(runId, projectId);
|
|
432
|
-
const run = await readJsonFile(
|
|
520
|
+
const run = await readJsonFile(
|
|
521
|
+
join2(candidate, "run.json")
|
|
522
|
+
);
|
|
433
523
|
if (run || await pathExists(join2(candidate, "events.ndjson"))) {
|
|
434
524
|
return candidate;
|
|
435
525
|
}
|
|
@@ -466,848 +556,46 @@ async function pathExists(path) {
|
|
|
466
556
|
}
|
|
467
557
|
}
|
|
468
558
|
|
|
469
|
-
// ../tracker-
|
|
470
|
-
import { createHash } from "crypto";
|
|
471
|
-
var DEFAULT_API_URL = "https://api.github.com/graphql";
|
|
472
|
-
var DEFAULT_PAGE_SIZE = 25;
|
|
473
|
-
var DEFAULT_NETWORK_TIMEOUT_MS = 3e4;
|
|
474
|
-
var RATE_LIMIT_THRESHOLD = 100;
|
|
475
|
-
var MAX_RATE_LIMIT_WAIT_MS = 6e4;
|
|
476
|
-
var GitHubTrackerError = class extends Error {
|
|
477
|
-
};
|
|
478
|
-
var GitHubTrackerHttpError = class extends GitHubTrackerError {
|
|
479
|
-
status;
|
|
480
|
-
details;
|
|
481
|
-
constructor(message, status, details) {
|
|
482
|
-
super(message);
|
|
483
|
-
this.status = status;
|
|
484
|
-
this.details = details;
|
|
485
|
-
}
|
|
486
|
-
};
|
|
487
|
-
var GitHubTrackerQueryError = class extends GitHubTrackerError {
|
|
488
|
-
};
|
|
489
|
-
var cachedGitHubGraphQLRateLimits = /* @__PURE__ */ new Map();
|
|
490
|
-
function normalizeProjectItem(projectId, item, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, priority = {}, rateLimits = null) {
|
|
491
|
-
if (item.content?.__typename !== "Issue") {
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
const fieldValues = extractFieldValues(item.fieldValues?.nodes ?? []);
|
|
495
|
-
const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
|
|
496
|
-
const repository = item.content.repository;
|
|
497
|
-
const blockedBy = (item.content.blockedBy?.nodes ?? []).flatMap((node) => node ? [
|
|
498
|
-
{
|
|
499
|
-
id: node.id,
|
|
500
|
-
identifier: `${node.repository.owner.login}/${node.repository.name}#${node.number}`,
|
|
501
|
-
state: normalizeBlockerState(node.state, lifecycle)
|
|
502
|
-
}
|
|
503
|
-
] : []);
|
|
504
|
-
const issueUpdatedAtMs = parseTimestampMs(item.content.updatedAt);
|
|
505
|
-
const itemUpdatedAtMs = parseTimestampMs(item.updatedAt);
|
|
506
|
-
const trackedUpdatedAt = itemUpdatedAtMs !== null && (issueUpdatedAtMs === null || itemUpdatedAtMs > issueUpdatedAtMs) ? item.updatedAt : item.content.updatedAt ?? item.updatedAt;
|
|
507
|
-
return {
|
|
508
|
-
id: item.content.id,
|
|
509
|
-
identifier: `${repository.owner.login}/${repository.name}#${item.content.number}`,
|
|
510
|
-
number: item.content.number,
|
|
511
|
-
title: item.content.title,
|
|
512
|
-
description: item.content.body,
|
|
513
|
-
priority: resolvePriority(item, priority),
|
|
514
|
-
state,
|
|
515
|
-
branchName: null,
|
|
516
|
-
url: item.content.url,
|
|
517
|
-
labels: (item.content.labels?.nodes ?? []).flatMap((label) => label?.name ? [label.name.toLowerCase()] : []).sort(),
|
|
518
|
-
blockedBy,
|
|
519
|
-
createdAt: item.content.createdAt,
|
|
520
|
-
updatedAt: trackedUpdatedAt,
|
|
521
|
-
repository: {
|
|
522
|
-
owner: repository.owner.login,
|
|
523
|
-
name: repository.name,
|
|
524
|
-
url: repository.url,
|
|
525
|
-
cloneUrl: deriveCloneUrl(repository.url)
|
|
526
|
-
},
|
|
527
|
-
tracker: {
|
|
528
|
-
adapter: "github-project",
|
|
529
|
-
bindingId: projectId,
|
|
530
|
-
itemId: item.id
|
|
531
|
-
},
|
|
532
|
-
metadata: fieldValues,
|
|
533
|
-
rateLimits
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
async function fetchProjectIssues(config, fetchImpl = fetch) {
|
|
537
|
-
const issues = [];
|
|
538
|
-
let cursor = null;
|
|
539
|
-
const priorityOptionIds = config.priorityFieldName ? await fetchPriorityOptionOrder(config, config.priorityFieldName, fetchImpl) : void 0;
|
|
540
|
-
const currentUserLogin = config.assignedOnly ? await fetchCurrentUserLogin(config, fetchImpl) : null;
|
|
541
|
-
let excludedCount = 0;
|
|
542
|
-
let latestRateLimits = null;
|
|
543
|
-
do {
|
|
544
|
-
const pageResult = await fetchProjectItemsPage(config, cursor, fetchImpl);
|
|
545
|
-
const page = pageResult.page;
|
|
546
|
-
latestRateLimits = pageResult.rateLimits ?? latestRateLimits;
|
|
547
|
-
const pageIssues = (page.nodes ?? []).flatMap((item) => {
|
|
548
|
-
if (!item) {
|
|
549
|
-
return [];
|
|
550
|
-
}
|
|
551
|
-
const normalized = normalizeProjectItem(config.projectId, item, config.lifecycle, {
|
|
552
|
-
fieldName: config.priorityFieldName,
|
|
553
|
-
optionIds: priorityOptionIds
|
|
554
|
-
}, latestRateLimits);
|
|
555
|
-
if (!normalized) {
|
|
556
|
-
return [];
|
|
557
|
-
}
|
|
558
|
-
if (currentUserLogin && !isIssueAssignedToLogin(item, currentUserLogin)) {
|
|
559
|
-
excludedCount += 1;
|
|
560
|
-
return [];
|
|
561
|
-
}
|
|
562
|
-
return [normalized];
|
|
563
|
-
});
|
|
564
|
-
issues.push(...pageIssues);
|
|
565
|
-
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
566
|
-
} while (cursor);
|
|
567
|
-
if (currentUserLogin) {
|
|
568
|
-
emitAssignedOnlyFilterEvent({
|
|
569
|
-
projectId: config.projectId,
|
|
570
|
-
currentUserLogin,
|
|
571
|
-
includedCount: issues.length,
|
|
572
|
-
excludedCount
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
if (latestRateLimits) {
|
|
576
|
-
for (const issue of issues) {
|
|
577
|
-
issue.rateLimits = latestRateLimits;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return issues;
|
|
581
|
-
}
|
|
582
|
-
async function fetchIssueStatesByIds(config, issueIds, fetchImpl = fetch) {
|
|
583
|
-
if (issueIds.length === 0) {
|
|
584
|
-
return [];
|
|
585
|
-
}
|
|
586
|
-
const issues = [];
|
|
587
|
-
for (const issueIdBatch of chunkValues([...new Set(issueIds)], 100)) {
|
|
588
|
-
const result = await executeGraphQLQueryWithMetadata(config, ISSUE_STATES_BY_IDS_QUERY, {
|
|
589
|
-
issueIds: issueIdBatch
|
|
590
|
-
}, fetchImpl);
|
|
591
|
-
const data = result.data;
|
|
592
|
-
const rateLimits = result.rateLimits;
|
|
593
|
-
for (const node of data.nodes ?? []) {
|
|
594
|
-
const projectItem = await resolveIssueProjectItemForStateLookup(config, node, fetchImpl);
|
|
595
|
-
const normalized = normalizeIssueStateLookupNode(config.projectId, node, projectItem, config.lifecycle, rateLimits);
|
|
596
|
-
if (normalized) {
|
|
597
|
-
issues.push(normalized);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return issues;
|
|
602
|
-
}
|
|
603
|
-
async function fetchProjectItemsPage(config, cursor, fetchImpl) {
|
|
604
|
-
const result = await executeGraphQLQueryWithMetadata(config, PROJECT_ITEMS_QUERY, {
|
|
605
|
-
projectId: config.projectId,
|
|
606
|
-
cursor,
|
|
607
|
-
pageSize: config.pageSize ?? DEFAULT_PAGE_SIZE
|
|
608
|
-
}, fetchImpl);
|
|
609
|
-
const data = result.data;
|
|
610
|
-
const items = data.node?.items;
|
|
611
|
-
if (!items) {
|
|
612
|
-
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include project items.");
|
|
613
|
-
}
|
|
614
|
-
return {
|
|
615
|
-
page: items,
|
|
616
|
-
rateLimits: result.rateLimits
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
var fetchGithubProjectIssues = fetchProjectIssues;
|
|
620
|
-
var fetchGithubIssueStatesByIds = fetchIssueStatesByIds;
|
|
621
|
-
async function fetchCurrentUserLogin(config, fetchImpl) {
|
|
622
|
-
const response = await fetchImpl(resolveRestUserApiUrl(config.apiUrl), {
|
|
623
|
-
method: "GET",
|
|
624
|
-
headers: {
|
|
625
|
-
authorization: `Bearer ${config.token}`,
|
|
626
|
-
"user-agent": "gh-symphony",
|
|
627
|
-
accept: "application/vnd.github+json"
|
|
628
|
-
},
|
|
629
|
-
signal: buildRequestSignal(config.timeoutMs)
|
|
630
|
-
});
|
|
631
|
-
if (!response.ok) {
|
|
632
|
-
const details = await response.text();
|
|
633
|
-
throw new GitHubTrackerHttpError(`GitHub REST request failed with status ${response.status}`, response.status, details);
|
|
634
|
-
}
|
|
635
|
-
const payload = await response.json();
|
|
636
|
-
if (!payload.login) {
|
|
637
|
-
throw new GitHubTrackerQueryError("GitHub REST response did not include the authenticated user login.");
|
|
638
|
-
}
|
|
639
|
-
return payload.login;
|
|
640
|
-
}
|
|
641
|
-
function isIssueAssignedToLogin(item, login) {
|
|
642
|
-
if (item.content?.__typename !== "Issue") {
|
|
643
|
-
return false;
|
|
644
|
-
}
|
|
645
|
-
return (item.content.assignees?.nodes ?? []).some((assignee) => assignee?.login === login);
|
|
646
|
-
}
|
|
647
|
-
function emitAssignedOnlyFilterEvent(input) {
|
|
648
|
-
console.info(JSON.stringify({
|
|
649
|
-
event: "tracker-assigned-only-filtered",
|
|
650
|
-
projectId: input.projectId,
|
|
651
|
-
currentUserLogin: input.currentUserLogin,
|
|
652
|
-
includedCount: input.includedCount,
|
|
653
|
-
excludedCount: input.excludedCount
|
|
654
|
-
}));
|
|
655
|
-
}
|
|
656
|
-
function extractFieldValues(nodes) {
|
|
657
|
-
return nodes.reduce((values, node) => {
|
|
658
|
-
const fieldName = node?.field?.name;
|
|
659
|
-
if (!fieldName) {
|
|
660
|
-
return values;
|
|
661
|
-
}
|
|
662
|
-
if (node.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.name) {
|
|
663
|
-
values[fieldName] = node.name;
|
|
664
|
-
}
|
|
665
|
-
if (node.__typename === "ProjectV2ItemFieldTextValue" && node.text) {
|
|
666
|
-
values[fieldName] = node.text;
|
|
667
|
-
}
|
|
668
|
-
return values;
|
|
669
|
-
}, {});
|
|
670
|
-
}
|
|
671
|
-
function normalizeIssueStateLookupNode(projectId, issue, projectItem, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, rateLimits = null) {
|
|
672
|
-
if (issue?.__typename !== "Issue") {
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
675
|
-
if (!projectItem) {
|
|
676
|
-
return null;
|
|
677
|
-
}
|
|
678
|
-
const fieldValues = extractFieldValues(projectItem.fieldValues?.nodes ?? []);
|
|
679
|
-
const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
|
|
680
|
-
const repository = issue.repository;
|
|
681
|
-
const identifier = `${repository.owner.login}/${repository.name}#${issue.number}`;
|
|
682
|
-
return {
|
|
683
|
-
id: issue.id,
|
|
684
|
-
identifier,
|
|
685
|
-
number: issue.number,
|
|
686
|
-
title: identifier,
|
|
687
|
-
description: null,
|
|
688
|
-
priority: null,
|
|
689
|
-
state,
|
|
690
|
-
branchName: null,
|
|
691
|
-
url: `${repository.url}/issues/${issue.number}`,
|
|
692
|
-
labels: [],
|
|
693
|
-
blockedBy: [],
|
|
694
|
-
createdAt: null,
|
|
695
|
-
updatedAt: projectItem.updatedAt ?? issue.updatedAt,
|
|
696
|
-
repository: {
|
|
697
|
-
owner: repository.owner.login,
|
|
698
|
-
name: repository.name,
|
|
699
|
-
url: repository.url,
|
|
700
|
-
cloneUrl: deriveCloneUrl(repository.url)
|
|
701
|
-
},
|
|
702
|
-
tracker: {
|
|
703
|
-
adapter: "github-project",
|
|
704
|
-
bindingId: projectId,
|
|
705
|
-
itemId: projectItem.id
|
|
706
|
-
},
|
|
707
|
-
metadata: fieldValues,
|
|
708
|
-
rateLimits
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
async function resolveIssueProjectItemForStateLookup(config, issue, fetchImpl) {
|
|
712
|
-
if (issue?.__typename !== "Issue") {
|
|
713
|
-
return null;
|
|
714
|
-
}
|
|
715
|
-
let connection = issue.projectItems;
|
|
716
|
-
let projectItem = findProjectItemByProjectId(connection?.nodes ?? [], config.projectId);
|
|
717
|
-
let cursor = connection?.pageInfo.endCursor ?? null;
|
|
718
|
-
while (!projectItem && connection?.pageInfo.hasNextPage) {
|
|
719
|
-
const nextPage = await fetchIssueProjectItemsPage(config, issue.id, cursor, fetchImpl);
|
|
720
|
-
projectItem = findProjectItemByProjectId(nextPage.nodes ?? [], config.projectId);
|
|
721
|
-
connection = nextPage;
|
|
722
|
-
cursor = nextPage.pageInfo.endCursor;
|
|
723
|
-
}
|
|
724
|
-
return projectItem;
|
|
725
|
-
}
|
|
726
|
-
async function fetchIssueProjectItemsPage(config, issueId, cursor, fetchImpl) {
|
|
727
|
-
const result = await executeGraphQLQueryWithMetadata(config, ISSUE_PROJECT_ITEMS_PAGE_QUERY, {
|
|
728
|
-
issueId,
|
|
729
|
-
cursor
|
|
730
|
-
}, fetchImpl);
|
|
731
|
-
const data = result.data;
|
|
732
|
-
const issue = data.node;
|
|
733
|
-
if (issue?.__typename !== "Issue" || !issue.projectItems) {
|
|
734
|
-
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include issue project items.");
|
|
735
|
-
}
|
|
736
|
-
return issue.projectItems;
|
|
737
|
-
}
|
|
738
|
-
function findProjectItemByProjectId(nodes, projectId) {
|
|
739
|
-
return nodes.find((item) => item?.project?.id === projectId) ?? null;
|
|
740
|
-
}
|
|
741
|
-
function resolvePriority(item, priority) {
|
|
742
|
-
if (!priority.fieldName || !priority.optionIds) {
|
|
743
|
-
return null;
|
|
744
|
-
}
|
|
745
|
-
for (const node of item.fieldValues?.nodes ?? []) {
|
|
746
|
-
if (node?.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.field?.name === priority.fieldName && node.optionId) {
|
|
747
|
-
return priority.optionIds[node.optionId] ?? null;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
function extractPriorityOptionOrder(fields, priorityFieldName) {
|
|
753
|
-
for (const field of fields) {
|
|
754
|
-
if (isSingleSelectProjectField(field) && field.name === priorityFieldName) {
|
|
755
|
-
let nextPriority = 0;
|
|
756
|
-
const optionEntries = (field.options ?? []).flatMap((option) => {
|
|
757
|
-
if (!option?.id) {
|
|
758
|
-
return [];
|
|
759
|
-
}
|
|
760
|
-
const entry = [option.id, nextPriority];
|
|
761
|
-
nextPriority += 1;
|
|
762
|
-
return [entry];
|
|
763
|
-
});
|
|
764
|
-
return Object.fromEntries(optionEntries);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
return void 0;
|
|
768
|
-
}
|
|
769
|
-
async function fetchPriorityOptionOrder(config, priorityFieldName, fetchImpl) {
|
|
770
|
-
const data = await executeGraphQLQuery(config, PROJECT_FIELDS_QUERY, { projectId: config.projectId }, fetchImpl);
|
|
771
|
-
return extractPriorityOptionOrder(data.node?.fields?.nodes ?? [], priorityFieldName);
|
|
772
|
-
}
|
|
773
|
-
function isSingleSelectProjectField(field) {
|
|
774
|
-
return field?.__typename === "ProjectV2SingleSelectField";
|
|
775
|
-
}
|
|
776
|
-
function deriveCloneUrl(repositoryUrl) {
|
|
777
|
-
if (repositoryUrl.startsWith("file://") || repositoryUrl.endsWith(".git")) {
|
|
778
|
-
return repositoryUrl;
|
|
779
|
-
}
|
|
780
|
-
return `${repositoryUrl}.git`;
|
|
781
|
-
}
|
|
782
|
-
function normalizeBlockerState(state, lifecycle) {
|
|
783
|
-
if (!state) {
|
|
784
|
-
return null;
|
|
785
|
-
}
|
|
786
|
-
const normalized = state.trim().toLowerCase();
|
|
787
|
-
if (normalized === "closed") {
|
|
788
|
-
return lifecycle.terminalStates[0] ?? state;
|
|
789
|
-
}
|
|
790
|
-
if (normalized === "open") {
|
|
791
|
-
return null;
|
|
792
|
-
}
|
|
793
|
-
return state;
|
|
794
|
-
}
|
|
795
|
-
function resolveRestUserApiUrl(apiUrl) {
|
|
796
|
-
const parsed = new URL(apiUrl ?? DEFAULT_API_URL);
|
|
797
|
-
const pathSegments = parsed.pathname.split("/").filter(Boolean);
|
|
798
|
-
if (pathSegments.at(-1) === "graphql") {
|
|
799
|
-
pathSegments.pop();
|
|
800
|
-
}
|
|
801
|
-
parsed.pathname = `/${pathSegments.join("/")}/user`.replace(/\/{2,}/g, "/");
|
|
802
|
-
parsed.search = "";
|
|
803
|
-
parsed.hash = "";
|
|
804
|
-
return parsed.toString();
|
|
805
|
-
}
|
|
806
|
-
function chunkValues(values, size) {
|
|
807
|
-
const chunks = [];
|
|
808
|
-
for (let index = 0; index < values.length; index += size) {
|
|
809
|
-
chunks.push(values.slice(index, index + size));
|
|
810
|
-
}
|
|
811
|
-
return chunks;
|
|
812
|
-
}
|
|
813
|
-
function buildRequestSignal(timeoutMs) {
|
|
814
|
-
return AbortSignal.timeout(resolveNetworkTimeoutMs(timeoutMs));
|
|
815
|
-
}
|
|
816
|
-
function resolveNetworkTimeoutMs(timeoutMs) {
|
|
817
|
-
if (timeoutMs !== void 0 && Number.isInteger(timeoutMs) && timeoutMs > 0) {
|
|
818
|
-
return timeoutMs;
|
|
819
|
-
}
|
|
820
|
-
return DEFAULT_NETWORK_TIMEOUT_MS;
|
|
821
|
-
}
|
|
822
|
-
async function executeGraphQLQuery(config, query, variables, fetchImpl) {
|
|
823
|
-
const result = await executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl);
|
|
824
|
-
return result.data;
|
|
825
|
-
}
|
|
826
|
-
async function executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl) {
|
|
827
|
-
const tokenFingerprint = fingerprintToken(config.token);
|
|
828
|
-
await guardGraphQLRateLimit(tokenFingerprint);
|
|
829
|
-
const response = await fetchImpl(config.apiUrl ?? DEFAULT_API_URL, {
|
|
830
|
-
method: "POST",
|
|
831
|
-
headers: {
|
|
832
|
-
"content-type": "application/json",
|
|
833
|
-
authorization: `Bearer ${config.token}`
|
|
834
|
-
},
|
|
835
|
-
body: JSON.stringify({
|
|
836
|
-
query,
|
|
837
|
-
variables
|
|
838
|
-
}),
|
|
839
|
-
signal: buildRequestSignal(config.timeoutMs)
|
|
840
|
-
});
|
|
841
|
-
if (!response.ok) {
|
|
842
|
-
const details = await response.text();
|
|
843
|
-
throw new GitHubTrackerHttpError(`GitHub GraphQL request failed with status ${response.status}`, response.status, details);
|
|
844
|
-
}
|
|
845
|
-
const payload = await response.json();
|
|
846
|
-
if (payload.errors?.length) {
|
|
847
|
-
throw new GitHubTrackerQueryError(payload.errors.map((error) => error.message).join("; "));
|
|
848
|
-
}
|
|
849
|
-
if (!payload.data) {
|
|
850
|
-
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include data.");
|
|
851
|
-
}
|
|
852
|
-
const data = payload.data;
|
|
853
|
-
const rateLimits = extractGitHubRateLimits(response.headers);
|
|
854
|
-
cachedGitHubGraphQLRateLimits.set(tokenFingerprint, rateLimits);
|
|
855
|
-
return {
|
|
856
|
-
data,
|
|
857
|
-
rateLimits
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
async function guardGraphQLRateLimit(tokenFingerprint) {
|
|
861
|
-
const rateLimit = cachedGitHubGraphQLRateLimits.get(tokenFingerprint) ?? null;
|
|
862
|
-
if (!rateLimit) {
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
const remaining = rateLimit.remaining;
|
|
866
|
-
if (remaining === null || remaining > RATE_LIMIT_THRESHOLD) {
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
const resetAtMs = parseTimestampMs(rateLimit.resetAt);
|
|
870
|
-
if (resetAtMs === null) {
|
|
871
|
-
throw new GitHubTrackerError("Rate limit near exhaustion");
|
|
872
|
-
}
|
|
873
|
-
const waitMs = Math.max(0, resetAtMs - Date.now());
|
|
874
|
-
if (waitMs > MAX_RATE_LIMIT_WAIT_MS) {
|
|
875
|
-
throw new GitHubTrackerError("Rate limit near exhaustion");
|
|
876
|
-
}
|
|
877
|
-
cachedGitHubGraphQLRateLimits.delete(tokenFingerprint);
|
|
878
|
-
if (waitMs > 0) {
|
|
879
|
-
await sleep(waitMs);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
function fingerprintToken(token) {
|
|
883
|
-
return createHash("sha256").update(token).digest("hex");
|
|
884
|
-
}
|
|
885
|
-
function extractGitHubRateLimits(headers) {
|
|
886
|
-
if (!headers || typeof headers.get !== "function") {
|
|
887
|
-
return null;
|
|
888
|
-
}
|
|
889
|
-
const limit = parseIntegerHeader(headers.get("x-ratelimit-limit"));
|
|
890
|
-
const remaining = parseIntegerHeader(headers.get("x-ratelimit-remaining"));
|
|
891
|
-
const used = parseIntegerHeader(headers.get("x-ratelimit-used"));
|
|
892
|
-
const reset = parseIntegerHeader(headers.get("x-ratelimit-reset"));
|
|
893
|
-
const resource = headers.get("x-ratelimit-resource");
|
|
894
|
-
if (limit === null && remaining === null && used === null && reset === null && resource === null) {
|
|
895
|
-
return null;
|
|
896
|
-
}
|
|
897
|
-
return {
|
|
898
|
-
source: "github",
|
|
899
|
-
limit,
|
|
900
|
-
remaining,
|
|
901
|
-
used,
|
|
902
|
-
reset,
|
|
903
|
-
resetAt: reset === null ? null : new Date(reset * 1e3).toISOString(),
|
|
904
|
-
resource
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
function parseIntegerHeader(value) {
|
|
908
|
-
if (value === null) {
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
const parsed = Number.parseInt(value, 10);
|
|
912
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
913
|
-
}
|
|
914
|
-
function parseTimestampMs(value) {
|
|
915
|
-
if (!value) {
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
const timestampMs = Date.parse(value);
|
|
919
|
-
return Number.isFinite(timestampMs) ? timestampMs : null;
|
|
920
|
-
}
|
|
921
|
-
function sleep(ms) {
|
|
922
|
-
return new Promise((resolve4) => {
|
|
923
|
-
setTimeout(resolve4, ms);
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
var PROJECT_ITEMS_QUERY = `
|
|
927
|
-
query ProjectItems($projectId: ID!, $cursor: String, $pageSize: Int!) {
|
|
928
|
-
node(id: $projectId) {
|
|
929
|
-
__typename
|
|
930
|
-
... on ProjectV2 {
|
|
931
|
-
items(first: $pageSize, after: $cursor) {
|
|
932
|
-
nodes {
|
|
933
|
-
id
|
|
934
|
-
updatedAt
|
|
935
|
-
fieldValues(first: 20) {
|
|
936
|
-
nodes {
|
|
937
|
-
__typename
|
|
938
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
939
|
-
name
|
|
940
|
-
optionId
|
|
941
|
-
field {
|
|
942
|
-
... on ProjectV2SingleSelectField {
|
|
943
|
-
name
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
... on ProjectV2ItemFieldTextValue {
|
|
948
|
-
text
|
|
949
|
-
field {
|
|
950
|
-
... on ProjectV2FieldCommon {
|
|
951
|
-
name
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
content {
|
|
958
|
-
__typename
|
|
959
|
-
... on Issue {
|
|
960
|
-
id
|
|
961
|
-
number
|
|
962
|
-
title
|
|
963
|
-
body
|
|
964
|
-
url
|
|
965
|
-
createdAt
|
|
966
|
-
updatedAt
|
|
967
|
-
labels(first: 20) {
|
|
968
|
-
nodes {
|
|
969
|
-
name
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
assignees(first: 20) {
|
|
973
|
-
nodes {
|
|
974
|
-
login
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
repository {
|
|
978
|
-
name
|
|
979
|
-
url
|
|
980
|
-
owner {
|
|
981
|
-
login
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
blockedBy(first: 100) {
|
|
985
|
-
nodes {
|
|
986
|
-
id
|
|
987
|
-
number
|
|
988
|
-
state
|
|
989
|
-
repository {
|
|
990
|
-
name
|
|
991
|
-
owner {
|
|
992
|
-
login
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
pageInfo {
|
|
1001
|
-
endCursor
|
|
1002
|
-
hasNextPage
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
`;
|
|
1009
|
-
var PROJECT_FIELDS_QUERY = `
|
|
1010
|
-
query ProjectFields($projectId: ID!) {
|
|
1011
|
-
node(id: $projectId) {
|
|
1012
|
-
__typename
|
|
1013
|
-
... on ProjectV2 {
|
|
1014
|
-
fields(first: 100) {
|
|
1015
|
-
nodes {
|
|
1016
|
-
__typename
|
|
1017
|
-
... on ProjectV2SingleSelectField {
|
|
1018
|
-
name
|
|
1019
|
-
options {
|
|
1020
|
-
id
|
|
1021
|
-
name
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
`;
|
|
1030
|
-
var ISSUE_STATES_BY_IDS_QUERY = `
|
|
1031
|
-
query IssueStatesByIds($issueIds: [ID!]!) {
|
|
1032
|
-
nodes(ids: $issueIds) {
|
|
1033
|
-
__typename
|
|
1034
|
-
... on Issue {
|
|
1035
|
-
id
|
|
1036
|
-
number
|
|
1037
|
-
updatedAt
|
|
1038
|
-
repository {
|
|
1039
|
-
name
|
|
1040
|
-
url
|
|
1041
|
-
owner {
|
|
1042
|
-
login
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
projectItems(first: 100, includeArchived: false) {
|
|
1046
|
-
nodes {
|
|
1047
|
-
id
|
|
1048
|
-
updatedAt
|
|
1049
|
-
project {
|
|
1050
|
-
id
|
|
1051
|
-
}
|
|
1052
|
-
fieldValues(first: 20) {
|
|
1053
|
-
nodes {
|
|
1054
|
-
__typename
|
|
1055
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
1056
|
-
name
|
|
1057
|
-
optionId
|
|
1058
|
-
field {
|
|
1059
|
-
... on ProjectV2SingleSelectField {
|
|
1060
|
-
name
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
... on ProjectV2ItemFieldTextValue {
|
|
1065
|
-
text
|
|
1066
|
-
field {
|
|
1067
|
-
... on ProjectV2FieldCommon {
|
|
1068
|
-
name
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
pageInfo {
|
|
1076
|
-
endCursor
|
|
1077
|
-
hasNextPage
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
`;
|
|
1084
|
-
var ISSUE_PROJECT_ITEMS_PAGE_QUERY = `
|
|
1085
|
-
query IssueProjectItemsPage($issueId: ID!, $cursor: String) {
|
|
1086
|
-
node(id: $issueId) {
|
|
1087
|
-
__typename
|
|
1088
|
-
... on Issue {
|
|
1089
|
-
id
|
|
1090
|
-
number
|
|
1091
|
-
updatedAt
|
|
1092
|
-
repository {
|
|
1093
|
-
name
|
|
1094
|
-
url
|
|
1095
|
-
owner {
|
|
1096
|
-
login
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
projectItems(first: 100, after: $cursor, includeArchived: false) {
|
|
1100
|
-
nodes {
|
|
1101
|
-
id
|
|
1102
|
-
updatedAt
|
|
1103
|
-
project {
|
|
1104
|
-
id
|
|
1105
|
-
}
|
|
1106
|
-
fieldValues(first: 20) {
|
|
1107
|
-
nodes {
|
|
1108
|
-
__typename
|
|
1109
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
1110
|
-
name
|
|
1111
|
-
optionId
|
|
1112
|
-
field {
|
|
1113
|
-
... on ProjectV2SingleSelectField {
|
|
1114
|
-
name
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
... on ProjectV2ItemFieldTextValue {
|
|
1119
|
-
text
|
|
1120
|
-
field {
|
|
1121
|
-
... on ProjectV2FieldCommon {
|
|
1122
|
-
name
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
pageInfo {
|
|
1130
|
-
endCursor
|
|
1131
|
-
hasNextPage
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
`;
|
|
1138
|
-
|
|
1139
|
-
// ../tracker-github/dist/orchestrator-adapter.js
|
|
1140
|
-
import { createHash as createHash2 } from "crypto";
|
|
1141
|
-
var githubProjectTrackerAdapter = {
|
|
1142
|
-
async listIssues(project, dependencies = {}) {
|
|
1143
|
-
return listProjectIssues(project, dependencies);
|
|
1144
|
-
},
|
|
1145
|
-
async listIssuesByStates(project, states, dependencies = {}) {
|
|
1146
|
-
if (states.length === 0) {
|
|
1147
|
-
return [];
|
|
1148
|
-
}
|
|
1149
|
-
const issues = await listProjectIssues(project, dependencies);
|
|
1150
|
-
const normalizedStates = new Set(states.map((state) => state.trim().toLowerCase()));
|
|
1151
|
-
return issues.filter((issue) => normalizedStates.has(issue.state.trim().toLowerCase()));
|
|
1152
|
-
},
|
|
1153
|
-
async fetchIssueStatesByIds(project, issueIds, dependencies = {}) {
|
|
1154
|
-
if (issueIds.length === 0) {
|
|
1155
|
-
return [];
|
|
1156
|
-
}
|
|
1157
|
-
return fetchProjectIssueStatesByIds(project, issueIds, dependencies);
|
|
1158
|
-
},
|
|
1159
|
-
buildWorkerEnvironment(project) {
|
|
1160
|
-
return {
|
|
1161
|
-
GITHUB_PROJECT_ID: requireTrackerSetting(project.tracker, "projectId")
|
|
1162
|
-
};
|
|
1163
|
-
},
|
|
1164
|
-
reviveIssue(project, run) {
|
|
1165
|
-
return {
|
|
1166
|
-
id: run.issueId,
|
|
1167
|
-
identifier: run.issueIdentifier,
|
|
1168
|
-
number: parseIssueNumber(run.issueIdentifier),
|
|
1169
|
-
title: run.issueTitle ?? run.issueIdentifier,
|
|
1170
|
-
description: null,
|
|
1171
|
-
priority: null,
|
|
1172
|
-
state: run.issueState,
|
|
1173
|
-
branchName: null,
|
|
1174
|
-
url: null,
|
|
1175
|
-
labels: [],
|
|
1176
|
-
blockedBy: [],
|
|
1177
|
-
createdAt: null,
|
|
1178
|
-
updatedAt: null,
|
|
1179
|
-
repository: run.repository,
|
|
1180
|
-
tracker: {
|
|
1181
|
-
adapter: "github-project",
|
|
1182
|
-
bindingId: project.tracker.bindingId,
|
|
1183
|
-
itemId: run.issueId
|
|
1184
|
-
},
|
|
1185
|
-
metadata: {}
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
};
|
|
1189
|
-
async function listProjectIssues(project, dependencies = {}) {
|
|
1190
|
-
const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
|
|
1191
|
-
const loadProjectIssues = () => fetchGithubProjectIssues(trackerConfig, dependencies.fetchImpl);
|
|
1192
|
-
return dependencies.projectItemsCache?.getOrLoad(buildProjectItemsCacheKey(trackerConfig, dependencies), loadProjectIssues) ?? loadProjectIssues();
|
|
1193
|
-
}
|
|
1194
|
-
async function fetchProjectIssueStatesByIds(project, issueIds, dependencies = {}) {
|
|
1195
|
-
const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
|
|
1196
|
-
return fetchGithubIssueStatesByIds(trackerConfig, [...issueIds], dependencies.fetchImpl);
|
|
1197
|
-
}
|
|
1198
|
-
function resolveGitHubTrackerConfig(project, dependencies = {}) {
|
|
1199
|
-
const token = dependencies.token ?? process.env.GITHUB_GRAPHQL_TOKEN;
|
|
1200
|
-
if (!token) {
|
|
1201
|
-
throw new Error("GITHUB_GRAPHQL_TOKEN environment variable is required. Run 'gh auth token' or set the variable.");
|
|
1202
|
-
}
|
|
1203
|
-
const githubProjectId = requireTrackerSetting(project.tracker, "projectId");
|
|
1204
|
-
return {
|
|
1205
|
-
projectId: githubProjectId,
|
|
1206
|
-
token,
|
|
1207
|
-
apiUrl: project.tracker.apiUrl,
|
|
1208
|
-
assignedOnly: readBooleanTrackerSetting(project.tracker, "assignedOnly"),
|
|
1209
|
-
priorityFieldName: readOptionalStringTrackerSetting(project.tracker, "priorityFieldName"),
|
|
1210
|
-
timeoutMs: readNumberTrackerSetting(project.tracker, "timeoutMs")
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
function buildProjectItemsCacheKey(config, _dependencies) {
|
|
1214
|
-
return JSON.stringify({
|
|
1215
|
-
adapter: "github-project",
|
|
1216
|
-
apiUrl: config.apiUrl,
|
|
1217
|
-
assignedOnly: config.assignedOnly ?? false,
|
|
1218
|
-
priorityFieldName: config.priorityFieldName ?? null,
|
|
1219
|
-
projectId: config.projectId,
|
|
1220
|
-
timeoutMs: config.timeoutMs,
|
|
1221
|
-
tokenFingerprint: hashToken(config.token)
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
function hashToken(token) {
|
|
1225
|
-
if (!token) {
|
|
1226
|
-
return null;
|
|
1227
|
-
}
|
|
1228
|
-
return createHash2("sha256").update(token).digest("hex");
|
|
1229
|
-
}
|
|
1230
|
-
var trackerAdapters = {
|
|
1231
|
-
"github-project": githubProjectTrackerAdapter
|
|
1232
|
-
};
|
|
1233
|
-
function resolveTrackerAdapter(tracker) {
|
|
1234
|
-
const adapter = trackerAdapters[tracker.adapter];
|
|
1235
|
-
if (!adapter) {
|
|
1236
|
-
throw new Error(`Unsupported tracker adapter: ${tracker.adapter}`);
|
|
1237
|
-
}
|
|
1238
|
-
return adapter;
|
|
1239
|
-
}
|
|
1240
|
-
function requireTrackerSetting(tracker, key) {
|
|
1241
|
-
const value = tracker.settings?.[key];
|
|
1242
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
1243
|
-
throw new Error(`Tracker adapter "${tracker.adapter}" requires the "${key}" setting.`);
|
|
1244
|
-
}
|
|
1245
|
-
return value;
|
|
1246
|
-
}
|
|
1247
|
-
function readBooleanTrackerSetting(tracker, key) {
|
|
1248
|
-
const value = tracker.settings?.[key];
|
|
1249
|
-
return value === true || value === "true";
|
|
1250
|
-
}
|
|
1251
|
-
function readNumberTrackerSetting(tracker, key) {
|
|
1252
|
-
const value = tracker.settings?.[key];
|
|
1253
|
-
if (value === void 0) {
|
|
1254
|
-
return void 0;
|
|
1255
|
-
}
|
|
1256
|
-
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
1257
|
-
return value;
|
|
1258
|
-
}
|
|
1259
|
-
if (typeof value === "string") {
|
|
1260
|
-
const parsed = Number(value);
|
|
1261
|
-
if (Number.isInteger(parsed) && parsed > 0) {
|
|
1262
|
-
return parsed;
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
throw new Error(`Tracker adapter "${tracker.adapter}" requires the "${key}" setting to be a positive integer when provided.`);
|
|
1266
|
-
}
|
|
1267
|
-
function readOptionalStringTrackerSetting(tracker, key) {
|
|
1268
|
-
const value = tracker.settings?.[key];
|
|
1269
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1270
|
-
}
|
|
1271
|
-
function parseIssueNumber(identifier) {
|
|
1272
|
-
const match = identifier.match(/#(\d+)$/);
|
|
1273
|
-
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// ../tracker-file/dist/file-tracker-adapter.js
|
|
559
|
+
// ../tracker-file/src/file-tracker-adapter.ts
|
|
1277
560
|
import { readFile as readFile2 } from "fs/promises";
|
|
1278
|
-
function
|
|
561
|
+
function requireTrackerSetting(project, key) {
|
|
1279
562
|
const value = project.tracker.settings?.[key];
|
|
1280
563
|
if (typeof value !== "string" || value.length === 0) {
|
|
1281
|
-
throw new Error(
|
|
564
|
+
throw new Error(
|
|
565
|
+
`Tracker adapter "file" requires the "${key}" setting.`
|
|
566
|
+
);
|
|
1282
567
|
}
|
|
1283
568
|
return value;
|
|
1284
569
|
}
|
|
1285
|
-
function
|
|
570
|
+
function parseIssueNumber(identifier) {
|
|
1286
571
|
const match = identifier.match(/#(\d+)$/);
|
|
1287
572
|
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
1288
573
|
}
|
|
1289
574
|
function isValidIssueShape(entry) {
|
|
1290
|
-
if (!entry || typeof entry !== "object")
|
|
1291
|
-
return false;
|
|
575
|
+
if (!entry || typeof entry !== "object") return false;
|
|
1292
576
|
const e = entry;
|
|
1293
577
|
return typeof e.id === "string" && typeof e.identifier === "string" && typeof e.state === "string" && e.repository !== null && typeof e.repository === "object" && e.tracker !== null && typeof e.tracker === "object";
|
|
1294
578
|
}
|
|
1295
579
|
var fileTrackerAdapter = {
|
|
1296
580
|
async listIssues(project) {
|
|
1297
|
-
const issuesPath =
|
|
581
|
+
const issuesPath = requireTrackerSetting(project, "issuesPath");
|
|
1298
582
|
try {
|
|
1299
583
|
const raw = await readFile2(issuesPath, "utf-8");
|
|
1300
584
|
const parsed = JSON.parse(raw);
|
|
1301
585
|
if (!Array.isArray(parsed)) {
|
|
1302
|
-
throw new Error(
|
|
586
|
+
throw new Error(
|
|
587
|
+
`Expected an array of issues in ${issuesPath}, got ${typeof parsed}`
|
|
588
|
+
);
|
|
1303
589
|
}
|
|
1304
590
|
const valid = [];
|
|
1305
591
|
for (let i = 0; i < parsed.length; i++) {
|
|
1306
592
|
if (isValidIssueShape(parsed[i])) {
|
|
1307
593
|
valid.push(parsed[i]);
|
|
1308
594
|
} else {
|
|
1309
|
-
process.stderr.write(
|
|
1310
|
-
`
|
|
595
|
+
process.stderr.write(
|
|
596
|
+
`[tracker-file] Skipping invalid issue at index ${i} in ${issuesPath}
|
|
597
|
+
`
|
|
598
|
+
);
|
|
1311
599
|
}
|
|
1312
600
|
}
|
|
1313
601
|
return valid;
|
|
@@ -1326,8 +614,12 @@ var fileTrackerAdapter = {
|
|
|
1326
614
|
return [];
|
|
1327
615
|
}
|
|
1328
616
|
const issues = await this.listIssues(project);
|
|
1329
|
-
const normalizedStates = new Set(
|
|
1330
|
-
|
|
617
|
+
const normalizedStates = new Set(
|
|
618
|
+
states.map((state) => state.trim().toLowerCase())
|
|
619
|
+
);
|
|
620
|
+
return issues.filter(
|
|
621
|
+
(issue) => normalizedStates.has(issue.state.trim().toLowerCase())
|
|
622
|
+
);
|
|
1331
623
|
},
|
|
1332
624
|
async fetchIssueStatesByIds(project, issueIds) {
|
|
1333
625
|
if (issueIds.length === 0) {
|
|
@@ -1346,7 +638,7 @@ var fileTrackerAdapter = {
|
|
|
1346
638
|
return {
|
|
1347
639
|
id: run.issueId,
|
|
1348
640
|
identifier: run.issueIdentifier,
|
|
1349
|
-
number:
|
|
641
|
+
number: parseIssueNumber(run.issueIdentifier),
|
|
1350
642
|
title: run.issueTitle ?? run.issueIdentifier,
|
|
1351
643
|
description: null,
|
|
1352
644
|
priority: null,
|
|
@@ -1368,24 +660,21 @@ var fileTrackerAdapter = {
|
|
|
1368
660
|
}
|
|
1369
661
|
};
|
|
1370
662
|
|
|
1371
|
-
// ../orchestrator/
|
|
663
|
+
// ../orchestrator/src/tracker-adapters.ts
|
|
1372
664
|
var localAdapters = /* @__PURE__ */ new Map([
|
|
1373
665
|
["file", fileTrackerAdapter]
|
|
1374
666
|
]);
|
|
1375
667
|
function resolveTrackerAdapter2(tracker) {
|
|
1376
668
|
const local = localAdapters.get(tracker.adapter);
|
|
1377
|
-
if (local)
|
|
1378
|
-
return local;
|
|
669
|
+
if (local) return local;
|
|
1379
670
|
return resolveTrackerAdapter(tracker);
|
|
1380
671
|
}
|
|
1381
672
|
|
|
1382
|
-
// ../orchestrator/
|
|
673
|
+
// ../orchestrator/src/service.ts
|
|
1383
674
|
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
1384
675
|
var DEFAULT_CONCURRENCY = 3;
|
|
1385
676
|
var DEFAULT_RETRY_BACKOFF_MS = 3e4;
|
|
1386
677
|
var CONTINUATION_RETRY_DELAY_MS = 1e3;
|
|
1387
|
-
var DEFAULT_GLOBAL_MAX_TURNS = 100;
|
|
1388
|
-
var DEFAULT_MAX_TOKENS = 256e3;
|
|
1389
678
|
var DEFAULT_WORKER_COMMAND = "node packages/worker/dist/index.js";
|
|
1390
679
|
var DEFAULT_MAX_NONPRODUCTIVE_TURNS = 3;
|
|
1391
680
|
var LOW_RATE_LIMIT_WARNING_THRESHOLD = 0.05;
|
|
@@ -1394,7 +683,7 @@ var STUCK_WORKER_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
|
1394
683
|
function isUsableWorkflowResolution(resolution) {
|
|
1395
684
|
return resolution.isValid || resolution.usedLastKnownGood;
|
|
1396
685
|
}
|
|
1397
|
-
function
|
|
686
|
+
function parseTimestampMs(value) {
|
|
1398
687
|
if (!value) {
|
|
1399
688
|
return null;
|
|
1400
689
|
}
|
|
@@ -1412,9 +701,11 @@ function parseFiniteNumber(value) {
|
|
|
1412
701
|
return null;
|
|
1413
702
|
}
|
|
1414
703
|
var OrchestratorService = class {
|
|
1415
|
-
store
|
|
1416
|
-
|
|
1417
|
-
|
|
704
|
+
constructor(store, projectConfig, dependencies = {}) {
|
|
705
|
+
this.store = store;
|
|
706
|
+
this.projectConfig = projectConfig;
|
|
707
|
+
this.dependencies = dependencies;
|
|
708
|
+
}
|
|
1418
709
|
projectPollIntervals = /* @__PURE__ */ new Map();
|
|
1419
710
|
activeWorkerPids = /* @__PURE__ */ new Set();
|
|
1420
711
|
workerStderrBuffers = /* @__PURE__ */ new Map();
|
|
@@ -1429,23 +720,25 @@ var OrchestratorService = class {
|
|
|
1429
720
|
sleepResolver = null;
|
|
1430
721
|
reconcilePromise = Promise.resolve();
|
|
1431
722
|
reconcileRequested = false;
|
|
1432
|
-
constructor(store, projectConfig, dependencies = {}) {
|
|
1433
|
-
this.store = store;
|
|
1434
|
-
this.projectConfig = projectConfig;
|
|
1435
|
-
this.dependencies = dependencies;
|
|
1436
|
-
}
|
|
1437
723
|
async run(options = {}) {
|
|
1438
724
|
this.running = true;
|
|
1439
|
-
await this.runSerialized(
|
|
725
|
+
await this.runSerialized(
|
|
726
|
+
() => this.performStartupCleanup(this.createTrackerDependencies())
|
|
727
|
+
);
|
|
1440
728
|
while (this.running) {
|
|
1441
729
|
try {
|
|
1442
|
-
const snapshot = await this.runOnceInternal(
|
|
730
|
+
const snapshot = await this.runOnceInternal(
|
|
731
|
+
options.issueIdentifier,
|
|
732
|
+
this.createTrackerDependencies()
|
|
733
|
+
);
|
|
1443
734
|
await this.notifyTick(snapshot);
|
|
1444
735
|
} catch (error) {
|
|
1445
736
|
if (options.once) {
|
|
1446
737
|
throw error;
|
|
1447
738
|
}
|
|
1448
|
-
this.writeStderr(
|
|
739
|
+
this.writeStderr(
|
|
740
|
+
`[orchestrator] run loop failed for ${this.projectConfig.projectId}: ${this.formatErrorMessage(error)}`
|
|
741
|
+
);
|
|
1449
742
|
}
|
|
1450
743
|
if (options.once || !this.running) {
|
|
1451
744
|
return;
|
|
@@ -1454,20 +747,39 @@ var OrchestratorService = class {
|
|
|
1454
747
|
}
|
|
1455
748
|
}
|
|
1456
749
|
async runOnce(options = {}) {
|
|
1457
|
-
return this.runOnceInternal(
|
|
750
|
+
return this.runOnceInternal(
|
|
751
|
+
options.issueIdentifier,
|
|
752
|
+
this.createTrackerDependencies()
|
|
753
|
+
);
|
|
1458
754
|
}
|
|
1459
755
|
async status() {
|
|
1460
756
|
return this.store.loadProjectStatus(this.projectConfig.projectId);
|
|
1461
757
|
}
|
|
1462
758
|
async statusForIssue(issueIdentifier) {
|
|
1463
|
-
const issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
1464
|
-
|
|
759
|
+
const issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
760
|
+
this.projectConfig.projectId
|
|
761
|
+
);
|
|
762
|
+
const issueRecord = issueRecords.find(
|
|
763
|
+
(record) => record.identifier === issueIdentifier
|
|
764
|
+
);
|
|
1465
765
|
if (!issueRecord) {
|
|
1466
766
|
return null;
|
|
1467
767
|
}
|
|
1468
|
-
const currentRunCandidate = issueRecord.currentRunId ? await this.store.loadRun(
|
|
1469
|
-
|
|
1470
|
-
|
|
768
|
+
const currentRunCandidate = issueRecord.currentRunId ? await this.store.loadRun(
|
|
769
|
+
issueRecord.currentRunId,
|
|
770
|
+
this.projectConfig.projectId
|
|
771
|
+
) : null;
|
|
772
|
+
const currentRun = isMatchingIssueRun(
|
|
773
|
+
currentRunCandidate,
|
|
774
|
+
this.projectConfig.projectId,
|
|
775
|
+
issueRecord.issueId,
|
|
776
|
+
issueIdentifier
|
|
777
|
+
) ? currentRunCandidate : await this.findLatestRunForIssue(issueRecord.issueId, issueIdentifier);
|
|
778
|
+
const recentEvents = currentRun === null ? [] : await this.store.loadRecentRunEvents(
|
|
779
|
+
currentRun.runId,
|
|
780
|
+
20,
|
|
781
|
+
currentRun.projectId
|
|
782
|
+
);
|
|
1471
783
|
const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
|
|
1472
784
|
const currentAttempt = currentRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
|
|
1473
785
|
return {
|
|
@@ -1504,7 +816,10 @@ var OrchestratorService = class {
|
|
|
1504
816
|
codex_session_logs: currentRun === null ? [] : [
|
|
1505
817
|
{
|
|
1506
818
|
label: "worker",
|
|
1507
|
-
path: join3(
|
|
819
|
+
path: join3(
|
|
820
|
+
this.store.runDir(currentRun.runId, currentRun.projectId),
|
|
821
|
+
"worker.log"
|
|
822
|
+
),
|
|
1508
823
|
url: null
|
|
1509
824
|
}
|
|
1510
825
|
]
|
|
@@ -1566,7 +881,9 @@ var OrchestratorService = class {
|
|
|
1566
881
|
if (this.dependencies.pollIntervalMs) {
|
|
1567
882
|
return this.dependencies.pollIntervalMs;
|
|
1568
883
|
}
|
|
1569
|
-
const configuredIntervals = [...this.projectPollIntervals.values()].filter(
|
|
884
|
+
const configuredIntervals = [...this.projectPollIntervals.values()].filter(
|
|
885
|
+
(value) => Number.isFinite(value) && value > 0
|
|
886
|
+
);
|
|
1570
887
|
return configuredIntervals.length ? Math.min(...configuredIntervals) : DEFAULT_POLL_INTERVAL_MS;
|
|
1571
888
|
}
|
|
1572
889
|
async reconcileProject(tenant, issueIdentifier, trackerDependencies = {}) {
|
|
@@ -1579,27 +896,57 @@ var OrchestratorService = class {
|
|
|
1579
896
|
let pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
1580
897
|
let rateLimits = null;
|
|
1581
898
|
let trackerRateLimits = null;
|
|
1582
|
-
let issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
1583
|
-
|
|
899
|
+
let issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
900
|
+
tenant.projectId
|
|
901
|
+
);
|
|
902
|
+
const allRuns = (await this.store.loadAllRuns()).filter(
|
|
903
|
+
(run) => run.projectId === tenant.projectId
|
|
904
|
+
);
|
|
1584
905
|
const activeRuns = allRuns.filter((run) => isActiveRunStatus(run.status));
|
|
1585
906
|
for (const run of activeRuns) {
|
|
1586
|
-
const outcome = await this.reconcileRun(
|
|
907
|
+
const outcome = await this.reconcileRun(
|
|
908
|
+
tenant,
|
|
909
|
+
run,
|
|
910
|
+
issueRecords,
|
|
911
|
+
trackerDependencies
|
|
912
|
+
);
|
|
1587
913
|
issueRecords = outcome.issueRecords;
|
|
1588
914
|
if (outcome.recovered) {
|
|
1589
915
|
recovered += 1;
|
|
1590
916
|
}
|
|
1591
917
|
}
|
|
1592
|
-
const reconciledRuns = (await this.store.loadAllRuns()).filter(
|
|
1593
|
-
|
|
918
|
+
const reconciledRuns = (await this.store.loadAllRuns()).filter(
|
|
919
|
+
(run) => run.projectId === tenant.projectId && isActiveRunStatus(run.status)
|
|
920
|
+
);
|
|
921
|
+
const projectRunsAfterReconcile = (await this.store.loadAllRuns()).filter(
|
|
922
|
+
(run) => run.projectId === tenant.projectId
|
|
923
|
+
);
|
|
1594
924
|
rateLimits = resolveProjectRateLimits(reconciledRuns, []);
|
|
1595
925
|
try {
|
|
1596
926
|
pollIntervalMs = await this.loadProjectPollInterval(tenant);
|
|
1597
|
-
const currentActiveRuns = (await this.store.loadAllRuns()).filter(
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
const
|
|
927
|
+
const currentActiveRuns = (await this.store.loadAllRuns()).filter(
|
|
928
|
+
(run) => run.projectId === tenant.projectId && isActiveRunStatus(run.status)
|
|
929
|
+
);
|
|
930
|
+
const {
|
|
931
|
+
runs: syncedActiveRuns,
|
|
932
|
+
issuesByIdentifier: syncedIssuesByIdentifier
|
|
933
|
+
} = await this.syncActiveRunIssueStates(
|
|
934
|
+
tenant,
|
|
935
|
+
trackerAdapter,
|
|
936
|
+
currentActiveRuns,
|
|
937
|
+
now
|
|
938
|
+
);
|
|
939
|
+
const issues = await trackerAdapter.listIssues(
|
|
940
|
+
tenant,
|
|
941
|
+
trackerDependencies
|
|
942
|
+
);
|
|
943
|
+
const filteredIssues = issueIdentifier ? issues.filter(
|
|
944
|
+
(issue) => issue.identifier === issueIdentifier
|
|
945
|
+
) : issues;
|
|
1601
946
|
const { candidates: actionableCandidates, lifecycle } = await this.resolveActionableCandidates(tenant, filteredIssues);
|
|
1602
|
-
const trackedIssuesByIdentifier = new Map(
|
|
947
|
+
const trackedIssuesByIdentifier = new Map(
|
|
948
|
+
syncedIssuesByIdentifier
|
|
949
|
+
);
|
|
1603
950
|
for (const issue of filteredIssues) {
|
|
1604
951
|
const existing = trackedIssuesByIdentifier.get(issue.identifier);
|
|
1605
952
|
trackedIssuesByIdentifier.set(issue.identifier, {
|
|
@@ -1620,17 +967,33 @@ var OrchestratorService = class {
|
|
|
1620
967
|
rateLimits: existing.rateLimits ?? issue.rateLimits ?? null
|
|
1621
968
|
});
|
|
1622
969
|
}
|
|
1623
|
-
rateLimits = resolveProjectRateLimits(
|
|
1624
|
-
|
|
970
|
+
rateLimits = resolveProjectRateLimits(
|
|
971
|
+
syncedActiveRuns,
|
|
972
|
+
trackedIssuesByIdentifier.values()
|
|
973
|
+
);
|
|
974
|
+
trackerRateLimits = resolveTrackerRateLimits(
|
|
975
|
+
trackedIssuesByIdentifier.values()
|
|
976
|
+
);
|
|
1625
977
|
const concurrency = await this.getProjectConcurrency(tenant);
|
|
1626
|
-
const currentlyActive = issueRecords.filter(
|
|
978
|
+
const currentlyActive = issueRecords.filter(
|
|
979
|
+
(record) => isIssueOrchestrationClaimed(record.state)
|
|
980
|
+
).length;
|
|
1627
981
|
const availableSlots = Math.max(0, concurrency - currentlyActive);
|
|
1628
|
-
const latestRunsByIssueId = buildLatestRunMapByIssueId(
|
|
982
|
+
const latestRunsByIssueId = buildLatestRunMapByIssueId(
|
|
983
|
+
projectRunsAfterReconcile
|
|
984
|
+
);
|
|
1629
985
|
const unscheduledCandidates = actionableCandidates.filter((issue) => {
|
|
1630
|
-
if (hasConvergenceLockedRun(
|
|
986
|
+
if (hasConvergenceLockedRun(
|
|
987
|
+
projectRunsAfterReconcile,
|
|
988
|
+
issue.id,
|
|
989
|
+
issue.state,
|
|
990
|
+
issue.updatedAt
|
|
991
|
+
)) {
|
|
1631
992
|
return false;
|
|
1632
993
|
}
|
|
1633
|
-
return !issueRecords.some(
|
|
994
|
+
return !issueRecords.some(
|
|
995
|
+
(record) => record.issueId === issue.id && isIssueOrchestrationClaimed(record.state)
|
|
996
|
+
);
|
|
1634
997
|
});
|
|
1635
998
|
const sortedCandidates = sortCandidatesForDispatch(unscheduledCandidates);
|
|
1636
999
|
const activeByState = /* @__PURE__ */ new Map();
|
|
@@ -1645,12 +1008,13 @@ var OrchestratorService = class {
|
|
|
1645
1008
|
if (this.shuttingDown) {
|
|
1646
1009
|
break;
|
|
1647
1010
|
}
|
|
1648
|
-
if (slotsRemaining <= 0)
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1011
|
+
if (slotsRemaining <= 0) break;
|
|
1012
|
+
if (await this.isFailureRetrySuppressedIssue(
|
|
1013
|
+
tenant,
|
|
1014
|
+
issue,
|
|
1015
|
+
issueRecords,
|
|
1016
|
+
latestRunsByIssueId.get(issue.id) ?? null
|
|
1017
|
+
)) {
|
|
1654
1018
|
continue;
|
|
1655
1019
|
}
|
|
1656
1020
|
const stateLimit = maxConcurrentByState[issue.state];
|
|
@@ -1660,11 +1024,14 @@ var OrchestratorService = class {
|
|
|
1660
1024
|
continue;
|
|
1661
1025
|
}
|
|
1662
1026
|
}
|
|
1663
|
-
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1027
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1028
|
+
{
|
|
1029
|
+
projectId: tenant.projectId,
|
|
1030
|
+
adapter: issue.tracker.adapter,
|
|
1031
|
+
issueSubjectId: issue.id
|
|
1032
|
+
},
|
|
1033
|
+
issue.identifier
|
|
1034
|
+
);
|
|
1668
1035
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
1669
1036
|
issueId: issue.id,
|
|
1670
1037
|
identifier: issue.identifier,
|
|
@@ -1700,10 +1067,15 @@ var OrchestratorService = class {
|
|
|
1700
1067
|
issueId: run.issueId,
|
|
1701
1068
|
issueState: issue.state
|
|
1702
1069
|
});
|
|
1703
|
-
this.logVerbose(
|
|
1070
|
+
this.logVerbose(
|
|
1071
|
+
`[dispatch] Issue ${issue.identifier} \u2192 run ${run.runId}`
|
|
1072
|
+
);
|
|
1704
1073
|
dispatched += 1;
|
|
1705
1074
|
slotsRemaining -= 1;
|
|
1706
|
-
activeByState.set(
|
|
1075
|
+
activeByState.set(
|
|
1076
|
+
issue.state,
|
|
1077
|
+
(activeByState.get(issue.state) ?? 0) + 1
|
|
1078
|
+
);
|
|
1707
1079
|
}
|
|
1708
1080
|
for (const issueRecord of issueRecords) {
|
|
1709
1081
|
if (!isIssueOrchestrationClaimed(issueRecord.state)) {
|
|
@@ -1714,8 +1086,17 @@ var OrchestratorService = class {
|
|
|
1714
1086
|
continue;
|
|
1715
1087
|
}
|
|
1716
1088
|
const persistedRun = issueRecord.currentRunId ? await this.store.loadRun(issueRecord.currentRunId, tenant.projectId) : null;
|
|
1717
|
-
const activeRun = syncedActiveRuns.find(
|
|
1718
|
-
|
|
1089
|
+
const activeRun = syncedActiveRuns.find(
|
|
1090
|
+
(run) => isMatchingIssueRun(
|
|
1091
|
+
run,
|
|
1092
|
+
tenant.projectId,
|
|
1093
|
+
issueRecord.issueId,
|
|
1094
|
+
issueRecord.identifier
|
|
1095
|
+
)
|
|
1096
|
+
) ?? persistedRun;
|
|
1097
|
+
const resolvedIssue = actionableCandidates.find(
|
|
1098
|
+
(candidate) => candidate.identifier === issue.identifier
|
|
1099
|
+
);
|
|
1719
1100
|
if (resolvedIssue) {
|
|
1720
1101
|
continue;
|
|
1721
1102
|
}
|
|
@@ -1734,9 +1115,15 @@ var OrchestratorService = class {
|
|
|
1734
1115
|
lastError: "Run suppressed because the tracker state is no longer actionable."
|
|
1735
1116
|
};
|
|
1736
1117
|
await this.store.saveRun(suppressedRun);
|
|
1737
|
-
this.logVerbose(
|
|
1118
|
+
this.logVerbose(
|
|
1119
|
+
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
1120
|
+
);
|
|
1738
1121
|
}
|
|
1739
|
-
issueRecords = releaseIssueOrchestration(
|
|
1122
|
+
issueRecords = releaseIssueOrchestration(
|
|
1123
|
+
issueRecords,
|
|
1124
|
+
issueRecord.issueId,
|
|
1125
|
+
now
|
|
1126
|
+
);
|
|
1740
1127
|
suppressed += 1;
|
|
1741
1128
|
}
|
|
1742
1129
|
const terminalIssuesByIdentifier = /* @__PURE__ */ new Map();
|
|
@@ -1752,14 +1139,28 @@ var OrchestratorService = class {
|
|
|
1752
1139
|
} catch (error) {
|
|
1753
1140
|
lastError = error instanceof Error ? error.message : "Unknown orchestration error";
|
|
1754
1141
|
}
|
|
1755
|
-
const effectivePollIntervalMs = resolveAdaptivePollIntervalMs(
|
|
1142
|
+
const effectivePollIntervalMs = resolveAdaptivePollIntervalMs(
|
|
1143
|
+
pollIntervalMs,
|
|
1144
|
+
trackerRateLimits
|
|
1145
|
+
);
|
|
1756
1146
|
if (effectivePollIntervalMs > pollIntervalMs && isLowRateLimit(trackerRateLimits, LOW_RATE_LIMIT_WARNING_THRESHOLD)) {
|
|
1757
|
-
this.writeStderr(
|
|
1147
|
+
this.writeStderr(
|
|
1148
|
+
`[orchestrator] low GitHub rate limit for ${tenant.projectId}: interval=${effectivePollIntervalMs}ms rateLimits=${JSON.stringify(
|
|
1149
|
+
trackerRateLimits
|
|
1150
|
+
)}`
|
|
1151
|
+
);
|
|
1758
1152
|
}
|
|
1759
1153
|
this.projectPollIntervals.set(tenant.projectId, effectivePollIntervalMs);
|
|
1760
|
-
await this.store.saveProjectIssueOrchestrations(
|
|
1761
|
-
|
|
1762
|
-
|
|
1154
|
+
await this.store.saveProjectIssueOrchestrations(
|
|
1155
|
+
tenant.projectId,
|
|
1156
|
+
issueRecords
|
|
1157
|
+
);
|
|
1158
|
+
const allTenantRuns = (await this.store.loadAllRuns()).filter(
|
|
1159
|
+
(run) => run.projectId === tenant.projectId
|
|
1160
|
+
);
|
|
1161
|
+
const latestRuns = allTenantRuns.filter(
|
|
1162
|
+
(run) => isActiveRunStatus(run.status)
|
|
1163
|
+
);
|
|
1763
1164
|
rateLimits = rateLimits ?? resolveProjectRateLimits(latestRuns, []);
|
|
1764
1165
|
const status = buildProjectSnapshot({
|
|
1765
1166
|
project: tenant,
|
|
@@ -1776,7 +1177,9 @@ var OrchestratorService = class {
|
|
|
1776
1177
|
async performStartupCleanup(trackerDependencies = {}) {
|
|
1777
1178
|
const tenant = this.projectConfig;
|
|
1778
1179
|
const now = this.now();
|
|
1779
|
-
const workspaceRecords = await this.store.loadIssueWorkspaces(
|
|
1180
|
+
const workspaceRecords = await this.store.loadIssueWorkspaces(
|
|
1181
|
+
tenant.projectId
|
|
1182
|
+
);
|
|
1780
1183
|
if (workspaceRecords.length === 0) {
|
|
1781
1184
|
return;
|
|
1782
1185
|
}
|
|
@@ -1784,10 +1187,20 @@ var OrchestratorService = class {
|
|
|
1784
1187
|
const workflowCache = /* @__PURE__ */ new Map();
|
|
1785
1188
|
let issues;
|
|
1786
1189
|
try {
|
|
1787
|
-
issues = await trackerAdapter.listIssuesByStates(
|
|
1190
|
+
issues = await trackerAdapter.listIssuesByStates(
|
|
1191
|
+
tenant,
|
|
1192
|
+
await this.resolveStartupCleanupTerminalStates(
|
|
1193
|
+
tenant,
|
|
1194
|
+
workspaceRecords,
|
|
1195
|
+
workflowCache
|
|
1196
|
+
),
|
|
1197
|
+
trackerDependencies
|
|
1198
|
+
);
|
|
1788
1199
|
} catch (error) {
|
|
1789
1200
|
const message = error instanceof Error ? error.message : "Unknown tracker error";
|
|
1790
|
-
console.warn(
|
|
1201
|
+
console.warn(
|
|
1202
|
+
`[orchestrator] Startup cleanup skipped for project ${tenant.projectId}: ${message}`
|
|
1203
|
+
);
|
|
1791
1204
|
return;
|
|
1792
1205
|
}
|
|
1793
1206
|
const issuesById = new Map(issues.map((issue) => [issue.id, issue]));
|
|
@@ -1800,17 +1213,28 @@ var OrchestratorService = class {
|
|
|
1800
1213
|
continue;
|
|
1801
1214
|
}
|
|
1802
1215
|
try {
|
|
1803
|
-
const resolution = await this.loadStartupCleanupWorkflow(
|
|
1216
|
+
const resolution = await this.loadStartupCleanupWorkflow(
|
|
1217
|
+
tenant,
|
|
1218
|
+
issue.repository,
|
|
1219
|
+
workflowCache
|
|
1220
|
+
);
|
|
1804
1221
|
if (!resolution.isValid) {
|
|
1805
1222
|
continue;
|
|
1806
1223
|
}
|
|
1807
1224
|
if (!isStateTerminal(issue.state, resolution.lifecycle)) {
|
|
1808
1225
|
continue;
|
|
1809
1226
|
}
|
|
1810
|
-
await this.cleanupTerminalIssueWorkspace(
|
|
1227
|
+
await this.cleanupTerminalIssueWorkspace(
|
|
1228
|
+
tenant,
|
|
1229
|
+
issue,
|
|
1230
|
+
now,
|
|
1231
|
+
resolution
|
|
1232
|
+
);
|
|
1811
1233
|
} catch (error) {
|
|
1812
1234
|
const message = error instanceof Error ? error.message : "Unknown startup cleanup error";
|
|
1813
|
-
console.warn(
|
|
1235
|
+
console.warn(
|
|
1236
|
+
`[orchestrator] Startup cleanup skipped workspace for ${issue.identifier}: ${message}`
|
|
1237
|
+
);
|
|
1814
1238
|
}
|
|
1815
1239
|
}
|
|
1816
1240
|
}
|
|
@@ -1821,7 +1245,9 @@ var OrchestratorService = class {
|
|
|
1821
1245
|
try {
|
|
1822
1246
|
await this.dependencies.onTick(snapshot);
|
|
1823
1247
|
} catch (error) {
|
|
1824
|
-
this.writeStderr(
|
|
1248
|
+
this.writeStderr(
|
|
1249
|
+
`[orchestrator] onTick callback failed: ${this.formatErrorMessage(error)}`
|
|
1250
|
+
);
|
|
1825
1251
|
}
|
|
1826
1252
|
}
|
|
1827
1253
|
formatErrorMessage(error) {
|
|
@@ -1832,11 +1258,18 @@ var OrchestratorService = class {
|
|
|
1832
1258
|
}
|
|
1833
1259
|
async resolveStartupCleanupTerminalStates(tenant, workspaceRecords, workflowCache) {
|
|
1834
1260
|
const terminalStates = /* @__PURE__ */ new Map();
|
|
1835
|
-
const repositories = this.resolveStartupCleanupRepositories(
|
|
1261
|
+
const repositories = this.resolveStartupCleanupRepositories(
|
|
1262
|
+
tenant,
|
|
1263
|
+
workspaceRecords
|
|
1264
|
+
);
|
|
1836
1265
|
for (const repository of repositories) {
|
|
1837
1266
|
let resolution;
|
|
1838
1267
|
try {
|
|
1839
|
-
resolution = await this.loadStartupCleanupWorkflow(
|
|
1268
|
+
resolution = await this.loadStartupCleanupWorkflow(
|
|
1269
|
+
tenant,
|
|
1270
|
+
repository,
|
|
1271
|
+
workflowCache
|
|
1272
|
+
);
|
|
1840
1273
|
} catch {
|
|
1841
1274
|
continue;
|
|
1842
1275
|
}
|
|
@@ -1860,14 +1293,20 @@ var OrchestratorService = class {
|
|
|
1860
1293
|
resolveStartupCleanupRepositories(tenant, workspaceRecords) {
|
|
1861
1294
|
const repositories = /* @__PURE__ */ new Map();
|
|
1862
1295
|
for (const repository of tenant.repositories) {
|
|
1863
|
-
repositories.set(
|
|
1296
|
+
repositories.set(
|
|
1297
|
+
this.startupCleanupRepositoryKey(repository.owner, repository.name),
|
|
1298
|
+
repository
|
|
1299
|
+
);
|
|
1864
1300
|
}
|
|
1865
1301
|
for (const workspaceRecord of workspaceRecords) {
|
|
1866
1302
|
const repository = this.parseWorkspaceRepositoryRef(workspaceRecord);
|
|
1867
1303
|
if (!repository) {
|
|
1868
1304
|
continue;
|
|
1869
1305
|
}
|
|
1870
|
-
const key = this.startupCleanupRepositoryKey(
|
|
1306
|
+
const key = this.startupCleanupRepositoryKey(
|
|
1307
|
+
repository.owner,
|
|
1308
|
+
repository.name
|
|
1309
|
+
);
|
|
1871
1310
|
if (!repositories.has(key)) {
|
|
1872
1311
|
repositories.set(key, repository);
|
|
1873
1312
|
}
|
|
@@ -1875,7 +1314,9 @@ var OrchestratorService = class {
|
|
|
1875
1314
|
return [...repositories.values()];
|
|
1876
1315
|
}
|
|
1877
1316
|
parseWorkspaceRepositoryRef(workspaceRecord) {
|
|
1878
|
-
const match = workspaceRecord.issueIdentifier.match(
|
|
1317
|
+
const match = workspaceRecord.issueIdentifier.match(
|
|
1318
|
+
/^([^/]+)\/([^#]+)#\d+$/
|
|
1319
|
+
);
|
|
1879
1320
|
if (!match) {
|
|
1880
1321
|
return null;
|
|
1881
1322
|
}
|
|
@@ -1899,7 +1340,9 @@ var OrchestratorService = class {
|
|
|
1899
1340
|
if (cachedResolution) {
|
|
1900
1341
|
return cachedResolution;
|
|
1901
1342
|
}
|
|
1902
|
-
const resolutionPromise = tenant.repositories.some(
|
|
1343
|
+
const resolutionPromise = tenant.repositories.some(
|
|
1344
|
+
(candidate) => candidate.owner === repository.owner && candidate.name === repository.name
|
|
1345
|
+
) ? this.loadProjectWorkflow(tenant, repository) : loadRepositoryWorkflow(repository.cloneUrl, repository);
|
|
1903
1346
|
workflowCache.set(cacheKey, resolutionPromise);
|
|
1904
1347
|
return resolutionPromise;
|
|
1905
1348
|
}
|
|
@@ -1921,7 +1364,11 @@ var OrchestratorService = class {
|
|
|
1921
1364
|
const workflowResolutionCache = /* @__PURE__ */ new Map();
|
|
1922
1365
|
this.workflowResolutionCache = workflowResolutionCache;
|
|
1923
1366
|
try {
|
|
1924
|
-
return await this.reconcileProject(
|
|
1367
|
+
return await this.reconcileProject(
|
|
1368
|
+
this.projectConfig,
|
|
1369
|
+
issueIdentifier,
|
|
1370
|
+
trackerDependencies
|
|
1371
|
+
);
|
|
1925
1372
|
} finally {
|
|
1926
1373
|
if (this.workflowResolutionCache === workflowResolutionCache) {
|
|
1927
1374
|
this.workflowResolutionCache = null;
|
|
@@ -1936,14 +1383,21 @@ var OrchestratorService = class {
|
|
|
1936
1383
|
};
|
|
1937
1384
|
}
|
|
1938
1385
|
async findLatestRunForIssue(issueId, issueIdentifier) {
|
|
1939
|
-
const matchingRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === this.projectConfig.projectId).filter(
|
|
1386
|
+
const matchingRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === this.projectConfig.projectId).filter(
|
|
1387
|
+
(run) => run.issueId === issueId || run.issueIdentifier === issueIdentifier
|
|
1388
|
+
).sort(
|
|
1389
|
+
(left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()
|
|
1390
|
+
);
|
|
1940
1391
|
return matchingRuns[0] ?? null;
|
|
1941
1392
|
}
|
|
1942
1393
|
async resolveActionableCandidates(tenant, issues) {
|
|
1943
1394
|
const candidates = [];
|
|
1944
1395
|
let lifecycle = null;
|
|
1945
1396
|
for (const issue of issues) {
|
|
1946
|
-
const resolution = await this.loadProjectWorkflow(
|
|
1397
|
+
const resolution = await this.loadProjectWorkflow(
|
|
1398
|
+
tenant,
|
|
1399
|
+
issue.repository
|
|
1400
|
+
);
|
|
1947
1401
|
if (!isUsableWorkflowResolution(resolution)) {
|
|
1948
1402
|
continue;
|
|
1949
1403
|
}
|
|
@@ -1956,7 +1410,10 @@ var OrchestratorService = class {
|
|
|
1956
1410
|
candidates.push(issue);
|
|
1957
1411
|
}
|
|
1958
1412
|
if (!lifecycle && tenant.repositories.length > 0) {
|
|
1959
|
-
const resolution = await this.loadProjectWorkflow(
|
|
1413
|
+
const resolution = await this.loadProjectWorkflow(
|
|
1414
|
+
tenant,
|
|
1415
|
+
tenant.repositories[0]
|
|
1416
|
+
);
|
|
1960
1417
|
if (isUsableWorkflowResolution(resolution)) {
|
|
1961
1418
|
lifecycle = resolution.lifecycle;
|
|
1962
1419
|
}
|
|
@@ -1983,7 +1440,9 @@ var OrchestratorService = class {
|
|
|
1983
1440
|
return false;
|
|
1984
1441
|
}
|
|
1985
1442
|
if (blockerRef.identifier) {
|
|
1986
|
-
const blockerIssue = issues.find(
|
|
1443
|
+
const blockerIssue = issues.find(
|
|
1444
|
+
(candidate) => candidate.identifier === blockerRef.identifier
|
|
1445
|
+
);
|
|
1987
1446
|
if (blockerIssue?.state) {
|
|
1988
1447
|
return !isStateTerminal(blockerIssue.state, lifecycle);
|
|
1989
1448
|
}
|
|
@@ -1999,24 +1458,42 @@ var OrchestratorService = class {
|
|
|
1999
1458
|
if (cachedResolution) {
|
|
2000
1459
|
return cachedResolution;
|
|
2001
1460
|
}
|
|
2002
|
-
const resolutionPromise = this.loadProjectWorkflowUncached(
|
|
1461
|
+
const resolutionPromise = this.loadProjectWorkflowUncached(
|
|
1462
|
+
tenant,
|
|
1463
|
+
repository
|
|
1464
|
+
);
|
|
2003
1465
|
pendingCache.set(cacheKey, resolutionPromise);
|
|
2004
1466
|
return resolutionPromise;
|
|
2005
1467
|
}
|
|
2006
1468
|
return this.loadProjectWorkflowUncached(tenant, repository);
|
|
2007
1469
|
}
|
|
2008
1470
|
async loadProjectWorkflowUncached(tenant, repository) {
|
|
2009
|
-
const cacheRoot = join3(
|
|
1471
|
+
const cacheRoot = join3(
|
|
1472
|
+
this.store.projectDir(tenant.projectId),
|
|
1473
|
+
"cache",
|
|
1474
|
+
repository.owner,
|
|
1475
|
+
repository.name
|
|
1476
|
+
);
|
|
2010
1477
|
const { repositoryDirectory, changed } = await syncRepositoryForRun({
|
|
2011
1478
|
repository,
|
|
2012
1479
|
targetDirectory: cacheRoot
|
|
2013
1480
|
});
|
|
2014
|
-
const resolution = await loadRepositoryWorkflow(
|
|
2015
|
-
|
|
1481
|
+
const resolution = await loadRepositoryWorkflow(
|
|
1482
|
+
repositoryDirectory,
|
|
1483
|
+
repository
|
|
1484
|
+
);
|
|
1485
|
+
return this.resolveWorkflowResolution(
|
|
1486
|
+
repository,
|
|
1487
|
+
cacheRoot,
|
|
1488
|
+
resolution,
|
|
1489
|
+
changed
|
|
1490
|
+
);
|
|
2016
1491
|
}
|
|
2017
|
-
async startRun(tenant, issue
|
|
1492
|
+
async startRun(tenant, issue) {
|
|
2018
1493
|
if (this.shuttingDown || !this.running) {
|
|
2019
|
-
throw new Error(
|
|
1494
|
+
throw new Error(
|
|
1495
|
+
"Orchestrator is shutting down and cannot start new runs."
|
|
1496
|
+
);
|
|
2020
1497
|
}
|
|
2021
1498
|
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
2022
1499
|
const now = this.now();
|
|
@@ -2029,12 +1506,24 @@ var OrchestratorService = class {
|
|
|
2029
1506
|
adapter: issue.tracker.adapter,
|
|
2030
1507
|
issueSubjectId
|
|
2031
1508
|
};
|
|
2032
|
-
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1509
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1510
|
+
identity,
|
|
1511
|
+
issue.identifier
|
|
1512
|
+
);
|
|
2033
1513
|
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(identity);
|
|
2034
|
-
const existingWorkspaceRecord = await this.store.loadIssueWorkspace(
|
|
1514
|
+
const existingWorkspaceRecord = await this.store.loadIssueWorkspace(
|
|
1515
|
+
tenant.projectId,
|
|
1516
|
+
preferredWorkspaceKey
|
|
1517
|
+
) ?? (legacyWorkspaceKey === preferredWorkspaceKey ? null : await this.store.loadIssueWorkspace(
|
|
1518
|
+
tenant.projectId,
|
|
1519
|
+
legacyWorkspaceKey
|
|
1520
|
+
));
|
|
2035
1521
|
const workspaceKey = existingWorkspaceRecord?.workspaceKey ?? preferredWorkspaceKey;
|
|
2036
1522
|
const projectDir = this.store.projectDir(tenant.projectId);
|
|
2037
|
-
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
1523
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
1524
|
+
projectDir,
|
|
1525
|
+
workspaceKey
|
|
1526
|
+
);
|
|
2038
1527
|
const repositoryDirectory = await ensureIssueWorkspaceRepository({
|
|
2039
1528
|
repository: issue.repository,
|
|
2040
1529
|
issueWorkspacePath
|
|
@@ -2054,14 +1543,20 @@ var OrchestratorService = class {
|
|
|
2054
1543
|
lastError: null
|
|
2055
1544
|
};
|
|
2056
1545
|
await this.store.saveIssueWorkspace(workspaceRecord);
|
|
2057
|
-
const afterCreateResult = await this.runHook(
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
1546
|
+
const afterCreateResult = await this.runHook(
|
|
1547
|
+
"after_create",
|
|
1548
|
+
tenant,
|
|
1549
|
+
repositoryDirectory,
|
|
1550
|
+
issue.repository,
|
|
1551
|
+
{
|
|
1552
|
+
projectId: tenant.projectId,
|
|
1553
|
+
workspaceKey,
|
|
1554
|
+
issueSubjectId,
|
|
1555
|
+
issueIdentifier: issue.identifier,
|
|
1556
|
+
workspacePath: issueWorkspacePath,
|
|
1557
|
+
repositoryPath: repositoryDirectory
|
|
1558
|
+
}
|
|
1559
|
+
);
|
|
2065
1560
|
if (afterCreateResult && afterCreateResult.outcome !== "success" && afterCreateResult.outcome !== "skipped") {
|
|
2066
1561
|
await this.store.appendRunEvent(runId, {
|
|
2067
1562
|
at: now.toISOString(),
|
|
@@ -2074,25 +1569,35 @@ var OrchestratorService = class {
|
|
|
2074
1569
|
}
|
|
2075
1570
|
const workflow = await this.loadProjectWorkflow(tenant, issue.repository);
|
|
2076
1571
|
if (!isUsableWorkflowResolution(workflow)) {
|
|
2077
|
-
throw new Error(
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
workflow.validationError ?? "Invalid repository WORKFLOW.md"
|
|
1574
|
+
);
|
|
2078
1575
|
}
|
|
2079
|
-
const allProjectRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId);
|
|
2080
|
-
const issueBudgetSnapshot = resolveIssueBudgetSnapshot(allProjectRuns, issue.id);
|
|
2081
1576
|
const promptVariables = buildPromptVariables(issue, {
|
|
2082
1577
|
attempt: null
|
|
2083
1578
|
// first execution
|
|
2084
1579
|
});
|
|
2085
|
-
const renderedPrompt = renderPrompt(
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
1580
|
+
const renderedPrompt = renderPrompt(
|
|
1581
|
+
workflow.promptTemplate,
|
|
1582
|
+
promptVariables
|
|
1583
|
+
);
|
|
1584
|
+
await this.runHook(
|
|
1585
|
+
"before_run",
|
|
1586
|
+
tenant,
|
|
1587
|
+
repositoryDirectory,
|
|
1588
|
+
issue.repository,
|
|
1589
|
+
{
|
|
1590
|
+
projectId: tenant.projectId,
|
|
1591
|
+
workspaceKey,
|
|
1592
|
+
issueSubjectId,
|
|
1593
|
+
issueIdentifier: issue.identifier,
|
|
1594
|
+
workspacePath: issueWorkspacePath,
|
|
1595
|
+
repositoryPath: repositoryDirectory,
|
|
1596
|
+
runId,
|
|
1597
|
+
state: issue.state
|
|
1598
|
+
}
|
|
1599
|
+
);
|
|
1600
|
+
const runtimeTimeouts = resolveWorkflowRuntimeTimeouts(workflow.workflow);
|
|
2096
1601
|
mkdirSync(runDir, { recursive: true });
|
|
2097
1602
|
const workerLogStream = (this.dependencies.createWriteStreamImpl ?? createWriteStream)(join3(runDir, "worker.log"), {
|
|
2098
1603
|
flags: "a"
|
|
@@ -2115,55 +1620,69 @@ var OrchestratorService = class {
|
|
|
2115
1620
|
}
|
|
2116
1621
|
workerLogAvailable = false;
|
|
2117
1622
|
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2118
|
-
this.writeStderr(
|
|
1623
|
+
this.writeStderr(
|
|
1624
|
+
`[orchestrator] failed to write worker log for ${runId}: ${message}`
|
|
1625
|
+
);
|
|
2119
1626
|
};
|
|
2120
|
-
const child = (this.dependencies.spawnImpl ?? spawn2)(
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
1627
|
+
const child = (this.dependencies.spawnImpl ?? spawn2)(
|
|
1628
|
+
"bash",
|
|
1629
|
+
["-lc", resolveWorkerCommand()],
|
|
1630
|
+
{
|
|
1631
|
+
cwd: process.cwd(),
|
|
1632
|
+
env: this.buildProjectExecutionEnv(tenant.projectId, {
|
|
1633
|
+
GITHUB_GRAPHQL_TOKEN: process.env.GITHUB_GRAPHQL_TOKEN ?? "",
|
|
1634
|
+
CODEX_PROJECT_ID: tenant.projectId,
|
|
1635
|
+
PROJECT_ID: tenant.projectId,
|
|
1636
|
+
WORKING_DIRECTORY: repositoryDirectory,
|
|
1637
|
+
WORKSPACE_RUNTIME_DIR: workspaceRuntimeDir,
|
|
1638
|
+
SYMPHONY_RUN_ID: runId,
|
|
1639
|
+
SYMPHONY_ISSUE_STATE: issue.state,
|
|
1640
|
+
SYMPHONY_ISSUE_ID: issue.id,
|
|
1641
|
+
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
1642
|
+
SYMPHONY_ISSUE_TITLE: issue.title,
|
|
1643
|
+
SYMPHONY_ISSUE_SUBJECT_ID: issueSubjectId,
|
|
1644
|
+
SYMPHONY_ISSUE_WORKSPACE_KEY: workspaceKey,
|
|
1645
|
+
SYMPHONY_TRACKER_ADAPTER: issue.tracker.adapter,
|
|
1646
|
+
SYMPHONY_TRACKER_BINDING_ID: issue.tracker.bindingId,
|
|
1647
|
+
SYMPHONY_TRACKER_ITEM_ID: issue.tracker.itemId,
|
|
1648
|
+
TARGET_REPOSITORY_CLONE_URL: issue.repository.cloneUrl,
|
|
1649
|
+
TARGET_REPOSITORY_OWNER: issue.repository.owner,
|
|
1650
|
+
TARGET_REPOSITORY_NAME: issue.repository.name,
|
|
1651
|
+
TARGET_REPOSITORY_URL: issue.repository.url,
|
|
1652
|
+
...trackerAdapter.buildWorkerEnvironment(tenant, issue),
|
|
1653
|
+
SYMPHONY_RENDERED_PROMPT: renderedPrompt,
|
|
1654
|
+
SYMPHONY_WORKFLOW_PATH: workflow.workflowPath ?? "",
|
|
1655
|
+
SYMPHONY_AGENT_COMMAND: resolveWorkflowRuntimeCommand(
|
|
1656
|
+
workflow.workflow
|
|
1657
|
+
),
|
|
1658
|
+
SYMPHONY_APPROVAL_POLICY: workflow.workflow.codex.approvalPolicy ?? "",
|
|
1659
|
+
SYMPHONY_THREAD_SANDBOX: workflow.workflow.codex.threadSandbox ?? "",
|
|
1660
|
+
SYMPHONY_TURN_SANDBOX_POLICY: workflow.workflow.codex.turnSandboxPolicy ?? "",
|
|
1661
|
+
SYMPHONY_MAX_TURNS: String(workflow.workflow.agent.maxTurns),
|
|
1662
|
+
SYMPHONY_MAX_NONPRODUCTIVE_TURNS: process.env.SYMPHONY_MAX_NONPRODUCTIVE_TURNS ?? String(DEFAULT_MAX_NONPRODUCTIVE_TURNS),
|
|
1663
|
+
// Clear legacy resume/budget env so fresh worker sessions do not
|
|
1664
|
+
// inherit stale process-level values.
|
|
1665
|
+
SYMPHONY_GLOBAL_MAX_TURNS: "",
|
|
1666
|
+
SYMPHONY_MAX_TOKENS: "",
|
|
1667
|
+
SYMPHONY_SESSION_TIMEOUT_MS: "",
|
|
1668
|
+
SYMPHONY_RESUME_THREAD_ID: "",
|
|
1669
|
+
SYMPHONY_CUMULATIVE_TURN_COUNT: "0",
|
|
1670
|
+
SYMPHONY_CUMULATIVE_INPUT_TOKENS: "0",
|
|
1671
|
+
SYMPHONY_CUMULATIVE_OUTPUT_TOKENS: "0",
|
|
1672
|
+
SYMPHONY_CUMULATIVE_TOTAL_TOKENS: "0",
|
|
1673
|
+
SYMPHONY_LAST_TURN_SUMMARY: "",
|
|
1674
|
+
SYMPHONY_SESSION_STARTED_AT: "",
|
|
1675
|
+
SYMPHONY_READ_TIMEOUT_MS: String(
|
|
1676
|
+
runtimeTimeouts.readTimeoutMs
|
|
1677
|
+
),
|
|
1678
|
+
SYMPHONY_TURN_TIMEOUT_MS: String(
|
|
1679
|
+
runtimeTimeouts.turnTimeoutMs
|
|
1680
|
+
)
|
|
1681
|
+
}),
|
|
1682
|
+
detached: true,
|
|
1683
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
1684
|
+
}
|
|
1685
|
+
);
|
|
2167
1686
|
const handleWorkerStderrChunk = (chunk) => {
|
|
2168
1687
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
2169
1688
|
if (workerLogAvailable) {
|
|
@@ -2200,7 +1719,9 @@ var OrchestratorService = class {
|
|
|
2200
1719
|
if (child.pid) {
|
|
2201
1720
|
this.retireWorkerPid(child.pid);
|
|
2202
1721
|
}
|
|
2203
|
-
this.logVerbose(
|
|
1722
|
+
this.logVerbose(
|
|
1723
|
+
`[worker-exited] ${runId} (code=${code ?? "null"}, signal=${signal ?? "null"})`
|
|
1724
|
+
);
|
|
2204
1725
|
};
|
|
2205
1726
|
const finalizeWorkerStderr = (code, signal) => {
|
|
2206
1727
|
if (workerExited || workerStderrFinalizing) {
|
|
@@ -2240,7 +1761,9 @@ var OrchestratorService = class {
|
|
|
2240
1761
|
}
|
|
2241
1762
|
child.on?.("error", (error) => {
|
|
2242
1763
|
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2243
|
-
this.writeStderr(
|
|
1764
|
+
this.writeStderr(
|
|
1765
|
+
`[orchestrator] worker process error for ${runId}: ${message}`
|
|
1766
|
+
);
|
|
2244
1767
|
finalizeWorkerStderr(null, null);
|
|
2245
1768
|
});
|
|
2246
1769
|
child.on?.("close", (code, signal) => {
|
|
@@ -2287,14 +1810,24 @@ var OrchestratorService = class {
|
|
|
2287
1810
|
issuesByIdentifier: /* @__PURE__ */ new Map()
|
|
2288
1811
|
};
|
|
2289
1812
|
}
|
|
2290
|
-
const issues = await trackerAdapter.fetchIssueStatesByIds(
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
1813
|
+
const issues = await trackerAdapter.fetchIssueStatesByIds(
|
|
1814
|
+
tenant,
|
|
1815
|
+
activeIssueIds,
|
|
1816
|
+
{
|
|
1817
|
+
fetchImpl: this.dependencies.fetchImpl
|
|
1818
|
+
}
|
|
1819
|
+
);
|
|
1820
|
+
const issuesByIdentifier = new Map(
|
|
1821
|
+
issues.map((issue) => [issue.identifier, issue])
|
|
1822
|
+
);
|
|
1823
|
+
const issueStateByIdentifier = new Map(
|
|
1824
|
+
issues.map((issue) => [issue.identifier, issue.state])
|
|
1825
|
+
);
|
|
2295
1826
|
const syncedRuns = [];
|
|
2296
1827
|
for (const run of activeRuns) {
|
|
2297
|
-
const currentTrackerState = issueStateByIdentifier.get(
|
|
1828
|
+
const currentTrackerState = issueStateByIdentifier.get(
|
|
1829
|
+
run.issueIdentifier
|
|
1830
|
+
);
|
|
2298
1831
|
if (!currentTrackerState || currentTrackerState === run.issueState) {
|
|
2299
1832
|
syncedRuns.push(run);
|
|
2300
1833
|
continue;
|
|
@@ -2317,8 +1850,10 @@ var OrchestratorService = class {
|
|
|
2317
1850
|
if (run.processId && this.isProcessRunning(run.processId)) {
|
|
2318
1851
|
const retryPolicy = await this.loadRetryPolicy(tenant, run.repository);
|
|
2319
1852
|
const configuredStallTimeoutMs = retryPolicy?.stallTimeoutMs ?? null;
|
|
2320
|
-
const lastActivityAtMs =
|
|
2321
|
-
|
|
1853
|
+
const lastActivityAtMs = parseTimestampMs(
|
|
1854
|
+
run.lastEventAt ?? run.startedAt
|
|
1855
|
+
);
|
|
1856
|
+
const startedAtMs = parseTimestampMs(run.startedAt);
|
|
2322
1857
|
const elapsedSinceLastActivityMs = lastActivityAtMs === null ? null : now.getTime() - lastActivityAtMs;
|
|
2323
1858
|
const runningSinceMs = startedAtMs === null ? null : now.getTime() - startedAtMs;
|
|
2324
1859
|
const isStalledByWorkflowTimeout = configuredStallTimeoutMs !== null && configuredStallTimeoutMs > 0 && elapsedSinceLastActivityMs !== null && elapsedSinceLastActivityMs > configuredStallTimeoutMs;
|
|
@@ -2329,9 +1864,13 @@ var OrchestratorService = class {
|
|
|
2329
1864
|
const elapsedSeconds = Math.round((elapsedMs ?? 0) / 1e3);
|
|
2330
1865
|
const timeoutSeconds = Math.round((timeoutMs ?? 0) / 1e3);
|
|
2331
1866
|
if (this.isVerboseLoggingEnabled()) {
|
|
2332
|
-
this.writeStderr(
|
|
1867
|
+
this.writeStderr(
|
|
1868
|
+
`[stall-detected] ${run.runId} (elapsed=${elapsedSeconds}s > ${timeoutSeconds}s)`
|
|
1869
|
+
);
|
|
2333
1870
|
} else {
|
|
2334
|
-
this.writeStderr(
|
|
1871
|
+
this.writeStderr(
|
|
1872
|
+
`[orchestrator] stuck worker detected for ${run.runId} (elapsed ${elapsedSeconds}s > ${timeoutSeconds}s) \u2014 sending SIGTERM`
|
|
1873
|
+
);
|
|
2335
1874
|
}
|
|
2336
1875
|
this.sendSignal(run.processId, "SIGTERM");
|
|
2337
1876
|
} else {
|
|
@@ -2344,11 +1883,14 @@ var OrchestratorService = class {
|
|
|
2344
1883
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2345
1884
|
issueId: run.issueId,
|
|
2346
1885
|
identifier: run.issueIdentifier,
|
|
2347
|
-
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
1886
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
1887
|
+
{
|
|
1888
|
+
projectId: tenant.projectId,
|
|
1889
|
+
adapter: tenant.tracker.adapter,
|
|
1890
|
+
issueSubjectId: run.issueSubjectId
|
|
1891
|
+
},
|
|
1892
|
+
run.issueIdentifier
|
|
1893
|
+
),
|
|
2352
1894
|
state: "running",
|
|
2353
1895
|
currentRunId: run.runId,
|
|
2354
1896
|
retryEntry: null,
|
|
@@ -2366,12 +1908,29 @@ var OrchestratorService = class {
|
|
|
2366
1908
|
const workerInfo = await this.fetchWorkerRunInfo(run);
|
|
2367
1909
|
const runWithTokens = {
|
|
2368
1910
|
...run,
|
|
2369
|
-
runtimeSession: buildRuntimeSession(
|
|
1911
|
+
runtimeSession: buildRuntimeSession(
|
|
1912
|
+
run.runtimeSession,
|
|
1913
|
+
workerInfo.sessionId,
|
|
1914
|
+
workerInfo.threadId,
|
|
1915
|
+
run.status === "running" ? "failed" : run.runtimeSession?.status ?? null,
|
|
1916
|
+
run.runtimeSession?.startedAt ?? run.startedAt ?? now.toISOString(),
|
|
1917
|
+
now.toISOString(),
|
|
1918
|
+
workerInfo.exitClassification
|
|
1919
|
+
),
|
|
2370
1920
|
threadId: workerInfo.threadId ?? run.threadId ?? null,
|
|
2371
|
-
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
1921
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
1922
|
+
run,
|
|
1923
|
+
workerInfo.turnCount ?? null
|
|
1924
|
+
),
|
|
2372
1925
|
tokenUsage: workerInfo.tokenUsage ?? run.tokenUsage,
|
|
2373
1926
|
lastEvent: workerInfo.lastEvent ?? run.lastEvent,
|
|
2374
|
-
lastTurnSummary: resolveLastTurnSummary(
|
|
1927
|
+
lastTurnSummary: resolveLastTurnSummary(
|
|
1928
|
+
run.lastTurnSummary,
|
|
1929
|
+
resolveLastTurnSummaryCandidate(
|
|
1930
|
+
workerInfo.lastEvent,
|
|
1931
|
+
workerInfo.lastError
|
|
1932
|
+
)
|
|
1933
|
+
),
|
|
2375
1934
|
lastEventAt: workerInfo.lastEventAt ?? run.lastEventAt ?? void 0,
|
|
2376
1935
|
lastEventAtSource: workerInfo.lastEventAtSource ?? run.lastEventAtSource ?? void 0,
|
|
2377
1936
|
executionPhase: workerInfo.executionPhase ?? run.executionPhase ?? null,
|
|
@@ -2397,46 +1956,68 @@ var OrchestratorService = class {
|
|
|
2397
1956
|
recovered: false
|
|
2398
1957
|
};
|
|
2399
1958
|
}
|
|
2400
|
-
if (await this.resolveRetryRestartAction(
|
|
1959
|
+
if (await this.resolveRetryRestartAction(
|
|
1960
|
+
tenant,
|
|
1961
|
+
run,
|
|
1962
|
+
trackerDependencies
|
|
1963
|
+
) === "release") {
|
|
2401
1964
|
return this.releaseRetryingRun(runWithTokens, issueRecords, now);
|
|
2402
1965
|
}
|
|
2403
1966
|
return this.restartRun(tenant, run, issueRecords, now, workerSessionId);
|
|
2404
1967
|
}
|
|
2405
|
-
if (workerInfo.exitClassification === "
|
|
1968
|
+
if (workerInfo.exitClassification === "convergence-detected") {
|
|
2406
1969
|
const completedRun = {
|
|
2407
1970
|
...runWithTokens,
|
|
2408
|
-
status:
|
|
1971
|
+
status: "failed",
|
|
2409
1972
|
processId: null,
|
|
2410
1973
|
updatedAt: now.toISOString(),
|
|
2411
1974
|
completedAt: now.toISOString(),
|
|
2412
1975
|
nextRetryAt: null,
|
|
2413
1976
|
retryKind: null,
|
|
2414
|
-
lastError:
|
|
2415
|
-
runPhase: runWithTokens.runPhase ??
|
|
1977
|
+
lastError: runWithTokens.lastError,
|
|
1978
|
+
runPhase: runWithTokens.runPhase ?? "failed"
|
|
2416
1979
|
};
|
|
2417
1980
|
await this.store.saveRun(completedRun);
|
|
2418
|
-
this.logVerbose(
|
|
1981
|
+
this.logVerbose(
|
|
1982
|
+
`[run-completed] ${completedRun.runId} status=${completedRun.status}`
|
|
1983
|
+
);
|
|
2419
1984
|
return {
|
|
2420
1985
|
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
2421
1986
|
recovered: false
|
|
2422
1987
|
};
|
|
2423
1988
|
}
|
|
2424
1989
|
if (run.issueWorkspaceKey) {
|
|
2425
|
-
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
1990
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
1991
|
+
this.store.projectDir(tenant.projectId),
|
|
1992
|
+
run.issueWorkspaceKey
|
|
1993
|
+
);
|
|
1994
|
+
await this.runHook(
|
|
1995
|
+
"after_run",
|
|
1996
|
+
tenant,
|
|
1997
|
+
run.workingDirectory,
|
|
1998
|
+
run.repository,
|
|
1999
|
+
{
|
|
2000
|
+
projectId: run.projectId,
|
|
2001
|
+
workspaceKey: run.issueWorkspaceKey,
|
|
2002
|
+
issueSubjectId: run.issueSubjectId,
|
|
2003
|
+
issueIdentifier: run.issueIdentifier,
|
|
2004
|
+
workspacePath: issueWorkspacePath,
|
|
2005
|
+
repositoryPath: run.workingDirectory,
|
|
2006
|
+
runId: run.runId,
|
|
2007
|
+
state: run.issueState
|
|
2008
|
+
}
|
|
2009
|
+
);
|
|
2436
2010
|
}
|
|
2437
|
-
const retryKind = await this.classifyRetryKind(
|
|
2011
|
+
const retryKind = await this.classifyRetryKind(
|
|
2012
|
+
tenant,
|
|
2013
|
+
run,
|
|
2014
|
+
trackerDependencies
|
|
2015
|
+
);
|
|
2438
2016
|
const failureRetryCount = retryKind === "failure" ? (this.resolveFailureRetryCount(issueRecords, run.issueId) ?? 0) + 1 : this.resolveFailureRetryCount(issueRecords, run.issueId) ?? 0;
|
|
2439
|
-
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2017
|
+
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2018
|
+
tenant,
|
|
2019
|
+
run.repository
|
|
2020
|
+
);
|
|
2440
2021
|
if (retryKind === "failure" && failureRetryCount >= maxFailureRetries) {
|
|
2441
2022
|
const lastError = [
|
|
2442
2023
|
`Run suppressed: ${MAX_FAILURE_RETRIES_EXCEEDED_REASON}.`,
|
|
@@ -2463,16 +2044,21 @@ var OrchestratorService = class {
|
|
|
2463
2044
|
issueId: run.issueId,
|
|
2464
2045
|
reason: MAX_FAILURE_RETRIES_EXCEEDED_REASON
|
|
2465
2046
|
});
|
|
2466
|
-
this.logVerbose(
|
|
2047
|
+
this.logVerbose(
|
|
2048
|
+
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
2049
|
+
);
|
|
2467
2050
|
return {
|
|
2468
2051
|
issueRecords: upsertIssueOrchestration(issueRecords, {
|
|
2469
2052
|
issueId: run.issueId,
|
|
2470
2053
|
identifier: run.issueIdentifier,
|
|
2471
|
-
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2054
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2055
|
+
{
|
|
2056
|
+
projectId: tenant.projectId,
|
|
2057
|
+
adapter: tenant.tracker.adapter,
|
|
2058
|
+
issueSubjectId: run.issueSubjectId
|
|
2059
|
+
},
|
|
2060
|
+
run.issueIdentifier
|
|
2061
|
+
),
|
|
2476
2062
|
state: "released",
|
|
2477
2063
|
failureRetryCount,
|
|
2478
2064
|
currentRunId: null,
|
|
@@ -2484,7 +2070,9 @@ var OrchestratorService = class {
|
|
|
2484
2070
|
}
|
|
2485
2071
|
let nextRetryAt;
|
|
2486
2072
|
if (retryKind === "continuation") {
|
|
2487
|
-
nextRetryAt = new Date(
|
|
2073
|
+
nextRetryAt = new Date(
|
|
2074
|
+
now.getTime() + CONTINUATION_RETRY_DELAY_MS
|
|
2075
|
+
).toISOString();
|
|
2488
2076
|
} else {
|
|
2489
2077
|
const retryOptions = await this.loadRetryPolicy(tenant, run.repository);
|
|
2490
2078
|
const backoffMs = this.dependencies.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
@@ -2505,16 +2093,23 @@ var OrchestratorService = class {
|
|
|
2505
2093
|
lastError: retryKind === "continuation" ? null : "Worker process exited unexpectedly."
|
|
2506
2094
|
};
|
|
2507
2095
|
await this.store.saveRun(retryRecord);
|
|
2508
|
-
this.logVerbose(
|
|
2509
|
-
|
|
2096
|
+
this.logVerbose(
|
|
2097
|
+
`[retry-scheduled] ${retryRecord.runId} kind=${retryKind} attempt=${retryRecord.attempt} nextAt=${nextRetryAt}`
|
|
2098
|
+
);
|
|
2099
|
+
this.logVerbose(
|
|
2100
|
+
`[run-completed] ${retryRecord.runId} status=${retryRecord.status}`
|
|
2101
|
+
);
|
|
2510
2102
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2511
2103
|
issueId: run.issueId,
|
|
2512
2104
|
identifier: run.issueIdentifier,
|
|
2513
|
-
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2105
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2106
|
+
{
|
|
2107
|
+
projectId: tenant.projectId,
|
|
2108
|
+
adapter: tenant.tracker.adapter,
|
|
2109
|
+
issueSubjectId: run.issueSubjectId
|
|
2110
|
+
},
|
|
2111
|
+
run.issueIdentifier
|
|
2112
|
+
),
|
|
2518
2113
|
state: "retry_queued",
|
|
2519
2114
|
completedOnce: retryKind === "continuation" ? true : void 0,
|
|
2520
2115
|
failureRetryCount,
|
|
@@ -2573,9 +2168,13 @@ var OrchestratorService = class {
|
|
|
2573
2168
|
if (!isOrchestratorChannelEvent(parsed)) {
|
|
2574
2169
|
return;
|
|
2575
2170
|
}
|
|
2576
|
-
void this.runSerialized(
|
|
2171
|
+
void this.runSerialized(
|
|
2172
|
+
() => this.applyWorkerChannelEvent(runId, parsed)
|
|
2173
|
+
).catch((error) => {
|
|
2577
2174
|
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2578
|
-
this.writeStderr(
|
|
2175
|
+
this.writeStderr(
|
|
2176
|
+
`[orchestrator] failed to apply worker channel event for ${runId}: ${message}`
|
|
2177
|
+
);
|
|
2579
2178
|
});
|
|
2580
2179
|
} catch {
|
|
2581
2180
|
}
|
|
@@ -2592,15 +2191,29 @@ var OrchestratorService = class {
|
|
|
2592
2191
|
...run,
|
|
2593
2192
|
updatedAt: nowIso2,
|
|
2594
2193
|
lastEvent: "heartbeat",
|
|
2595
|
-
lastTurnSummary: resolveLastTurnSummary(
|
|
2194
|
+
lastTurnSummary: resolveLastTurnSummary(
|
|
2195
|
+
run.lastTurnSummary,
|
|
2196
|
+
event.lastError
|
|
2197
|
+
),
|
|
2596
2198
|
lastEventAt: persistedLastEventAt,
|
|
2597
2199
|
lastEventAtSource: event.lastEventAt != null ? "event-channel" : run.lastEventAtSource ?? null,
|
|
2598
2200
|
tokenUsage: event.tokenUsage,
|
|
2599
2201
|
rateLimits: event.rateLimits,
|
|
2600
|
-
runtimeSession: buildRuntimeSession(
|
|
2202
|
+
runtimeSession: buildRuntimeSession(
|
|
2203
|
+
run.runtimeSession,
|
|
2204
|
+
resolveChannelSessionId(event.sessionInfo),
|
|
2205
|
+
event.sessionInfo?.threadId ?? null,
|
|
2206
|
+
"active",
|
|
2207
|
+
run.startedAt ?? run.runtimeSession?.startedAt ?? nowIso2,
|
|
2208
|
+
nowIso2,
|
|
2209
|
+
event.sessionInfo?.exitClassification ?? null
|
|
2210
|
+
),
|
|
2601
2211
|
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2602
2212
|
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
2603
|
-
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2213
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2214
|
+
run,
|
|
2215
|
+
event.sessionInfo?.turnCount ?? null
|
|
2216
|
+
),
|
|
2604
2217
|
executionPhase: event.executionPhase ?? run.executionPhase,
|
|
2605
2218
|
runPhase: event.runPhase ?? run.runPhase,
|
|
2606
2219
|
lastError: event.lastError
|
|
@@ -2661,15 +2274,29 @@ var OrchestratorService = class {
|
|
|
2661
2274
|
...run,
|
|
2662
2275
|
updatedAt: nowIso,
|
|
2663
2276
|
lastEvent: event.event ?? run.lastEvent ?? null,
|
|
2664
|
-
lastTurnSummary: resolveLastTurnSummary(
|
|
2277
|
+
lastTurnSummary: resolveLastTurnSummary(
|
|
2278
|
+
run.lastTurnSummary,
|
|
2279
|
+
resolveLastTurnSummaryCandidate(event.event, event.lastError)
|
|
2280
|
+
),
|
|
2665
2281
|
lastEventAt: event.lastEventAt,
|
|
2666
2282
|
lastEventAtSource: "event-channel",
|
|
2667
2283
|
tokenUsage: event.tokenUsage ?? run.tokenUsage,
|
|
2668
2284
|
rateLimits: event.rateLimits ?? run.rateLimits ?? null,
|
|
2669
|
-
runtimeSession: buildRuntimeSession(
|
|
2285
|
+
runtimeSession: buildRuntimeSession(
|
|
2286
|
+
run.runtimeSession,
|
|
2287
|
+
resolveChannelSessionId(event.sessionInfo),
|
|
2288
|
+
event.sessionInfo?.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2289
|
+
"active",
|
|
2290
|
+
run.startedAt ?? run.runtimeSession?.startedAt ?? nowIso,
|
|
2291
|
+
nowIso,
|
|
2292
|
+
event.sessionInfo?.exitClassification ?? null
|
|
2293
|
+
),
|
|
2670
2294
|
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2671
2295
|
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
2672
|
-
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2296
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2297
|
+
run,
|
|
2298
|
+
event.sessionInfo?.turnCount ?? null
|
|
2299
|
+
),
|
|
2673
2300
|
executionPhase: event.executionPhase ?? run.executionPhase ?? null,
|
|
2674
2301
|
runPhase: event.runPhase ?? run.runPhase ?? null,
|
|
2675
2302
|
lastError: event.lastError ?? run.lastError
|
|
@@ -2734,7 +2361,11 @@ var OrchestratorService = class {
|
|
|
2734
2361
|
*/
|
|
2735
2362
|
async classifyRetryKind(tenant, run, trackerDependencies = {}) {
|
|
2736
2363
|
try {
|
|
2737
|
-
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2364
|
+
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2365
|
+
tenant,
|
|
2366
|
+
run.issueIdentifier,
|
|
2367
|
+
trackerDependencies
|
|
2368
|
+
);
|
|
2738
2369
|
if (!eligibleContext) {
|
|
2739
2370
|
return "failure";
|
|
2740
2371
|
}
|
|
@@ -2742,17 +2373,22 @@ var OrchestratorService = class {
|
|
|
2742
2373
|
if (!isUsableWorkflowResolution(resolution)) {
|
|
2743
2374
|
return "failure";
|
|
2744
2375
|
}
|
|
2745
|
-
return this.isIssueCandidateEligible(
|
|
2376
|
+
return this.isIssueCandidateEligible(
|
|
2377
|
+
eligibleContext.issue,
|
|
2378
|
+
resolution.lifecycle,
|
|
2379
|
+
eligibleContext.issues
|
|
2380
|
+
) ? "continuation" : "failure";
|
|
2746
2381
|
} catch {
|
|
2747
2382
|
return "failure";
|
|
2748
2383
|
}
|
|
2749
2384
|
}
|
|
2750
2385
|
async resolveRetryRestartAction(tenant, run, trackerDependencies = {}) {
|
|
2751
2386
|
try {
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2387
|
+
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2388
|
+
tenant,
|
|
2389
|
+
run.issueIdentifier,
|
|
2390
|
+
trackerDependencies
|
|
2391
|
+
);
|
|
2756
2392
|
if (!eligibleContext) {
|
|
2757
2393
|
return "release";
|
|
2758
2394
|
}
|
|
@@ -2760,7 +2396,11 @@ var OrchestratorService = class {
|
|
|
2760
2396
|
if (!isUsableWorkflowResolution(resolution)) {
|
|
2761
2397
|
return "restart";
|
|
2762
2398
|
}
|
|
2763
|
-
return this.isIssueCandidateEligible(
|
|
2399
|
+
return this.isIssueCandidateEligible(
|
|
2400
|
+
eligibleContext.issue,
|
|
2401
|
+
resolution.lifecycle,
|
|
2402
|
+
eligibleContext.issues
|
|
2403
|
+
) ? "restart" : "release";
|
|
2764
2404
|
} catch {
|
|
2765
2405
|
return "restart";
|
|
2766
2406
|
}
|
|
@@ -2771,7 +2411,9 @@ var OrchestratorService = class {
|
|
|
2771
2411
|
fetchImpl: this.dependencies.fetchImpl,
|
|
2772
2412
|
...trackerDependencies
|
|
2773
2413
|
});
|
|
2774
|
-
const issue = issues.find(
|
|
2414
|
+
const issue = issues.find(
|
|
2415
|
+
(candidate) => candidate.identifier === issueIdentifier
|
|
2416
|
+
);
|
|
2775
2417
|
return issue ? { issue, issues } : null;
|
|
2776
2418
|
}
|
|
2777
2419
|
async fetchWorkerRunInfo(run) {
|
|
@@ -2795,12 +2437,20 @@ var OrchestratorService = class {
|
|
|
2795
2437
|
async readPersistedWorkerTokenUsage(run) {
|
|
2796
2438
|
const artifactPaths = [
|
|
2797
2439
|
join3(run.workspaceRuntimeDir, "token-usage.json"),
|
|
2798
|
-
join3(
|
|
2440
|
+
join3(
|
|
2441
|
+
run.workspaceRuntimeDir,
|
|
2442
|
+
".orchestrator",
|
|
2443
|
+
"runs",
|
|
2444
|
+
run.runId,
|
|
2445
|
+
"token-usage.json"
|
|
2446
|
+
)
|
|
2799
2447
|
];
|
|
2800
2448
|
for (const artifactPath of artifactPaths) {
|
|
2801
2449
|
try {
|
|
2802
2450
|
const raw = await readFile3(artifactPath, "utf8");
|
|
2803
|
-
const tokenUsage = JSON.parse(
|
|
2451
|
+
const tokenUsage = JSON.parse(
|
|
2452
|
+
raw
|
|
2453
|
+
);
|
|
2804
2454
|
if (hasTokenUsage(tokenUsage)) {
|
|
2805
2455
|
return tokenUsage;
|
|
2806
2456
|
}
|
|
@@ -2821,7 +2471,10 @@ var OrchestratorService = class {
|
|
|
2821
2471
|
if (!isUsableWorkflowResolution(workflowResolution)) {
|
|
2822
2472
|
return null;
|
|
2823
2473
|
}
|
|
2824
|
-
const hookEnv = this.buildProjectExecutionEnv(
|
|
2474
|
+
const hookEnv = this.buildProjectExecutionEnv(
|
|
2475
|
+
tenant.projectId,
|
|
2476
|
+
buildHookEnv(context)
|
|
2477
|
+
);
|
|
2825
2478
|
return executeWorkspaceHook({
|
|
2826
2479
|
kind,
|
|
2827
2480
|
hooks: workflowResolution.workflow.hooks,
|
|
@@ -2839,14 +2492,24 @@ var OrchestratorService = class {
|
|
|
2839
2492
|
return readEnvFile(envPath);
|
|
2840
2493
|
} catch (error) {
|
|
2841
2494
|
const message = error instanceof Error ? error.message : "Unknown error occurred.";
|
|
2842
|
-
(this.dependencies.stderr ?? process.stderr).write(
|
|
2843
|
-
`
|
|
2495
|
+
(this.dependencies.stderr ?? process.stderr).write(
|
|
2496
|
+
`[warn] Failed to load project env for ${projectId} from ${envPath}: ${message}
|
|
2497
|
+
`
|
|
2498
|
+
);
|
|
2844
2499
|
return {};
|
|
2845
2500
|
}
|
|
2846
2501
|
}
|
|
2847
2502
|
buildProjectExecutionEnv(projectId, env) {
|
|
2848
|
-
const inheritedEnv = Object.fromEntries(
|
|
2849
|
-
|
|
2503
|
+
const inheritedEnv = Object.fromEntries(
|
|
2504
|
+
Object.entries(process.env).filter(
|
|
2505
|
+
(entry) => typeof entry[1] === "string"
|
|
2506
|
+
)
|
|
2507
|
+
);
|
|
2508
|
+
const explicitEnv = Object.fromEntries(
|
|
2509
|
+
Object.entries(env).filter(
|
|
2510
|
+
(entry) => typeof entry[1] === "string"
|
|
2511
|
+
)
|
|
2512
|
+
);
|
|
2850
2513
|
return {
|
|
2851
2514
|
...this.readProjectEnv(projectId),
|
|
2852
2515
|
...inheritedEnv,
|
|
@@ -2862,19 +2525,18 @@ var OrchestratorService = class {
|
|
|
2862
2525
|
lastError: "Superseded by recovered run."
|
|
2863
2526
|
};
|
|
2864
2527
|
await this.store.saveRun(supersededRecord);
|
|
2865
|
-
const issue = resolveTrackerAdapter2(tenant.tracker).reviveIssue(
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
});
|
|
2528
|
+
const issue = resolveTrackerAdapter2(tenant.tracker).reviveIssue(
|
|
2529
|
+
tenant,
|
|
2530
|
+
run
|
|
2531
|
+
);
|
|
2532
|
+
const restarted = await this.startRun(tenant, issue);
|
|
2871
2533
|
const recoveredRecord = {
|
|
2872
2534
|
...restarted,
|
|
2873
2535
|
attempt: run.attempt,
|
|
2874
2536
|
retryKind: run.retryKind ?? "recovery",
|
|
2875
2537
|
createdAt: run.createdAt,
|
|
2876
2538
|
issueWorkspaceKey: run.issueWorkspaceKey,
|
|
2877
|
-
threadId:
|
|
2539
|
+
threadId: null,
|
|
2878
2540
|
cumulativeTurnCount: resolvePersistedCumulativeTurnCount(run),
|
|
2879
2541
|
lastTurnSummary: run.lastTurnSummary ?? null,
|
|
2880
2542
|
turnCount: 0
|
|
@@ -2892,11 +2554,14 @@ var OrchestratorService = class {
|
|
|
2892
2554
|
issueRecords: upsertIssueOrchestration(issueRecords, {
|
|
2893
2555
|
issueId: recoveredRecord.issueId,
|
|
2894
2556
|
identifier: recoveredRecord.issueIdentifier,
|
|
2895
|
-
workspaceKey: recoveredRecord.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2557
|
+
workspaceKey: recoveredRecord.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2558
|
+
{
|
|
2559
|
+
projectId: tenant.projectId,
|
|
2560
|
+
adapter: tenant.tracker.adapter,
|
|
2561
|
+
issueSubjectId: recoveredRecord.issueSubjectId
|
|
2562
|
+
},
|
|
2563
|
+
recoveredRecord.issueIdentifier
|
|
2564
|
+
),
|
|
2900
2565
|
state: "running",
|
|
2901
2566
|
currentRunId: recoveredRecord.runId,
|
|
2902
2567
|
retryEntry: null,
|
|
@@ -2917,34 +2582,40 @@ var OrchestratorService = class {
|
|
|
2917
2582
|
lastError: "Retry canceled because the tracker issue is no longer actionable."
|
|
2918
2583
|
};
|
|
2919
2584
|
await this.store.saveRun(suppressedRun);
|
|
2920
|
-
this.logVerbose(
|
|
2585
|
+
this.logVerbose(
|
|
2586
|
+
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
2587
|
+
);
|
|
2921
2588
|
return {
|
|
2922
2589
|
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
2923
2590
|
recovered: false
|
|
2924
2591
|
};
|
|
2925
2592
|
}
|
|
2926
2593
|
async loadProjectPollInterval(tenant) {
|
|
2927
|
-
const intervals = await Promise.all(
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2594
|
+
const intervals = await Promise.all(
|
|
2595
|
+
tenant.repositories.map(async (repository) => {
|
|
2596
|
+
const resolution = await this.loadProjectWorkflow(tenant, repository);
|
|
2597
|
+
return isUsableWorkflowResolution(resolution) ? resolution.workflow.polling.intervalMs : NaN;
|
|
2598
|
+
})
|
|
2599
|
+
);
|
|
2600
|
+
const validIntervals = intervals.filter(
|
|
2601
|
+
(value) => Number.isFinite(value) && value > 0
|
|
2602
|
+
);
|
|
2932
2603
|
return validIntervals.length ? Math.min(...validIntervals) : DEFAULT_POLL_INTERVAL_MS;
|
|
2933
2604
|
}
|
|
2934
2605
|
async loadProjectMaxConcurrentByState(tenant) {
|
|
2935
2606
|
const result = {};
|
|
2936
|
-
const resolutions = await Promise.all(
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2607
|
+
const resolutions = await Promise.all(
|
|
2608
|
+
tenant.repositories.map(async (repository) => {
|
|
2609
|
+
try {
|
|
2610
|
+
return await this.loadProjectWorkflow(tenant, repository);
|
|
2611
|
+
} catch {
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
})
|
|
2615
|
+
);
|
|
2943
2616
|
for (const resolution of resolutions) {
|
|
2944
|
-
if (!resolution)
|
|
2945
|
-
|
|
2946
|
-
if (!isUsableWorkflowResolution(resolution))
|
|
2947
|
-
continue;
|
|
2617
|
+
if (!resolution) continue;
|
|
2618
|
+
if (!isUsableWorkflowResolution(resolution)) continue;
|
|
2948
2619
|
const stateLimits = resolution.workflow.agent.maxConcurrentAgentsByState;
|
|
2949
2620
|
for (const [state, limit] of Object.entries(stateLimits)) {
|
|
2950
2621
|
const existing = result[state];
|
|
@@ -2963,7 +2634,7 @@ var OrchestratorService = class {
|
|
|
2963
2634
|
return {
|
|
2964
2635
|
baseDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.retryBaseDelayMs,
|
|
2965
2636
|
maxDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.maxRetryBackoffMs,
|
|
2966
|
-
stallTimeoutMs: resolution.workflow.
|
|
2637
|
+
stallTimeoutMs: resolveWorkflowRuntimeTimeouts(resolution.workflow).stallTimeoutMs
|
|
2967
2638
|
};
|
|
2968
2639
|
} catch {
|
|
2969
2640
|
if (!this.dependencies.retryBackoffMs) {
|
|
@@ -2980,15 +2651,22 @@ var OrchestratorService = class {
|
|
|
2980
2651
|
if (this.dependencies.concurrency !== void 0) {
|
|
2981
2652
|
return this.dependencies.concurrency;
|
|
2982
2653
|
}
|
|
2983
|
-
const limits = await Promise.all(
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2654
|
+
const limits = await Promise.all(
|
|
2655
|
+
project.repositories.map(async (repository) => {
|
|
2656
|
+
try {
|
|
2657
|
+
const resolution = await this.loadProjectWorkflow(
|
|
2658
|
+
project,
|
|
2659
|
+
repository
|
|
2660
|
+
);
|
|
2661
|
+
return isUsableWorkflowResolution(resolution) ? resolution.workflow.agent.maxConcurrentAgents : NaN;
|
|
2662
|
+
} catch {
|
|
2663
|
+
return NaN;
|
|
2664
|
+
}
|
|
2665
|
+
})
|
|
2666
|
+
);
|
|
2667
|
+
const validLimits = limits.filter(
|
|
2668
|
+
(value) => Number.isFinite(value) && value >= 0
|
|
2669
|
+
);
|
|
2992
2670
|
return validLimits.length ? Math.min(...validLimits) : DEFAULT_CONCURRENCY;
|
|
2993
2671
|
}
|
|
2994
2672
|
async resolveWorkflowResolution(repository, cacheRoot, resolution, changed) {
|
|
@@ -3002,7 +2680,10 @@ var OrchestratorService = class {
|
|
|
3002
2680
|
};
|
|
3003
2681
|
let workflowPath = effectiveResolution.workflowPath;
|
|
3004
2682
|
try {
|
|
3005
|
-
workflowPath = await this.persistLastKnownGoodWorkflow(
|
|
2683
|
+
workflowPath = await this.persistLastKnownGoodWorkflow(
|
|
2684
|
+
cacheRoot,
|
|
2685
|
+
effectiveResolution
|
|
2686
|
+
) ?? effectiveResolution.workflowPath;
|
|
3006
2687
|
} catch {
|
|
3007
2688
|
workflowPath = effectiveResolution.workflowPath;
|
|
3008
2689
|
}
|
|
@@ -3017,8 +2698,10 @@ var OrchestratorService = class {
|
|
|
3017
2698
|
const message = resolution.validationError ?? "Invalid repository WORKFLOW.md";
|
|
3018
2699
|
const previousMessage = this.lastReportedWorkflowErrors.get(cacheKey);
|
|
3019
2700
|
if (changed || previousMessage !== message) {
|
|
3020
|
-
process.stderr.write(
|
|
3021
|
-
`
|
|
2701
|
+
process.stderr.write(
|
|
2702
|
+
`[orchestrator] failed to reload WORKFLOW.md for ${repository.owner}/${repository.name}: ${message}
|
|
2703
|
+
`
|
|
2704
|
+
);
|
|
3022
2705
|
this.lastReportedWorkflowErrors.set(cacheKey, message);
|
|
3023
2706
|
}
|
|
3024
2707
|
if (!cached) {
|
|
@@ -3108,10 +2791,22 @@ var OrchestratorService = class {
|
|
|
3108
2791
|
adapter: issue.tracker.adapter,
|
|
3109
2792
|
issueSubjectId
|
|
3110
2793
|
};
|
|
3111
|
-
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
2794
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
2795
|
+
identity,
|
|
2796
|
+
issue.identifier
|
|
2797
|
+
);
|
|
3112
2798
|
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(identity);
|
|
3113
2799
|
const orchestrationRecord = (await this.store.loadProjectIssueOrchestrations(tenant.projectId)).find((record) => record.issueId === issue.id);
|
|
3114
|
-
const workspaceRecord = (orchestrationRecord ? await this.store.loadIssueWorkspace(
|
|
2800
|
+
const workspaceRecord = (orchestrationRecord ? await this.store.loadIssueWorkspace(
|
|
2801
|
+
tenant.projectId,
|
|
2802
|
+
orchestrationRecord.workspaceKey
|
|
2803
|
+
) : null) ?? await this.store.loadIssueWorkspace(
|
|
2804
|
+
tenant.projectId,
|
|
2805
|
+
preferredWorkspaceKey
|
|
2806
|
+
) ?? (legacyWorkspaceKey === preferredWorkspaceKey ? null : await this.store.loadIssueWorkspace(
|
|
2807
|
+
tenant.projectId,
|
|
2808
|
+
legacyWorkspaceKey
|
|
2809
|
+
));
|
|
3115
2810
|
if (!workspaceRecord || workspaceRecord.status === "removed") {
|
|
3116
2811
|
return;
|
|
3117
2812
|
}
|
|
@@ -3121,17 +2816,26 @@ var OrchestratorService = class {
|
|
|
3121
2816
|
updatedAt: now.toISOString()
|
|
3122
2817
|
};
|
|
3123
2818
|
await this.store.saveIssueWorkspace(pendingRecord);
|
|
3124
|
-
const hookResult = await this.runHook(
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
2819
|
+
const hookResult = await this.runHook(
|
|
2820
|
+
"before_remove",
|
|
2821
|
+
tenant,
|
|
2822
|
+
workspaceRecord.repositoryPath,
|
|
2823
|
+
issue.repository,
|
|
2824
|
+
{
|
|
2825
|
+
projectId: tenant.projectId,
|
|
2826
|
+
workspaceKey: workspaceRecord.workspaceKey,
|
|
2827
|
+
issueSubjectId,
|
|
2828
|
+
issueIdentifier: issue.identifier,
|
|
2829
|
+
workspacePath: workspaceRecord.workspacePath,
|
|
2830
|
+
repositoryPath: workspaceRecord.repositoryPath
|
|
2831
|
+
},
|
|
2832
|
+
workflowResolution
|
|
2833
|
+
);
|
|
3132
2834
|
if (hookResult && hookResult.outcome !== "success" && hookResult.outcome !== "skipped") {
|
|
3133
2835
|
const errorMessage = hookResult.error ?? `before_remove hook ${hookResult.outcome}`;
|
|
3134
|
-
console.warn(
|
|
2836
|
+
console.warn(
|
|
2837
|
+
`[orchestrator] before_remove hook failed for ${issue.identifier}; continuing cleanup: ${errorMessage}`
|
|
2838
|
+
);
|
|
3135
2839
|
}
|
|
3136
2840
|
try {
|
|
3137
2841
|
await rm3(workspaceRecord.workspacePath, { recursive: true, force: true });
|
|
@@ -3149,19 +2853,26 @@ var OrchestratorService = class {
|
|
|
3149
2853
|
return issueRecords.find((record) => record.issueId === issueId)?.failureRetryCount ?? null;
|
|
3150
2854
|
}
|
|
3151
2855
|
async isFailureRetrySuppressedIssue(tenant, issue, issueRecords, latestRun) {
|
|
3152
|
-
const issueRecord = issueRecords.find(
|
|
2856
|
+
const issueRecord = issueRecords.find(
|
|
2857
|
+
(record) => record.issueId === issue.id || record.identifier === issue.identifier
|
|
2858
|
+
) ?? null;
|
|
3153
2859
|
if (!issueRecord || issueRecord.failureRetryCount <= 0) {
|
|
3154
2860
|
return false;
|
|
3155
2861
|
}
|
|
3156
|
-
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2862
|
+
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2863
|
+
tenant,
|
|
2864
|
+
issue.repository
|
|
2865
|
+
);
|
|
3157
2866
|
if (issueRecord.failureRetryCount < maxFailureRetries) {
|
|
3158
2867
|
return false;
|
|
3159
2868
|
}
|
|
3160
2869
|
if (!latestRun || latestRun.status !== "suppressed" || latestRun.issueState !== issue.state || !latestRun.lastError?.includes(MAX_FAILURE_RETRIES_EXCEEDED_REASON)) {
|
|
3161
2870
|
return false;
|
|
3162
2871
|
}
|
|
3163
|
-
const issueUpdatedAtMs =
|
|
3164
|
-
const suppressedAtMs =
|
|
2872
|
+
const issueUpdatedAtMs = parseTimestampMs(issue.updatedAt);
|
|
2873
|
+
const suppressedAtMs = parseTimestampMs(
|
|
2874
|
+
latestRun.completedAt ?? latestRun.updatedAt
|
|
2875
|
+
);
|
|
3165
2876
|
if (issueUpdatedAtMs === null || suppressedAtMs === null) {
|
|
3166
2877
|
return true;
|
|
3167
2878
|
}
|
|
@@ -3177,7 +2888,9 @@ var OrchestratorService = class {
|
|
|
3177
2888
|
}
|
|
3178
2889
|
};
|
|
3179
2890
|
function hasTokenUsage(tokenUsage) {
|
|
3180
|
-
return Boolean(
|
|
2891
|
+
return Boolean(
|
|
2892
|
+
tokenUsage && (tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0 || tokenUsage.totalTokens > 0)
|
|
2893
|
+
);
|
|
3181
2894
|
}
|
|
3182
2895
|
function isRecord(value) {
|
|
3183
2896
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -3189,7 +2902,9 @@ function resolveProjectRateLimits(runs, issues) {
|
|
|
3189
2902
|
if (!isRecord(run.rateLimits)) {
|
|
3190
2903
|
continue;
|
|
3191
2904
|
}
|
|
3192
|
-
const timestamp =
|
|
2905
|
+
const timestamp = parseTimestampMs(
|
|
2906
|
+
run.lastEventAt ?? run.updatedAt ?? run.startedAt
|
|
2907
|
+
);
|
|
3193
2908
|
const sortableTimestamp = timestamp ?? -Infinity;
|
|
3194
2909
|
if (sortableTimestamp >= latestRunTimestamp) {
|
|
3195
2910
|
latestRunTimestamp = sortableTimestamp;
|
|
@@ -3268,56 +2983,21 @@ function resolvePersistedCumulativeTurnCount(run) {
|
|
|
3268
2983
|
return run.cumulativeTurnCount ?? run.turnCount ?? 0;
|
|
3269
2984
|
}
|
|
3270
2985
|
function hasConvergenceLockedRun(runs, issueId, issueState, issueUpdatedAt) {
|
|
3271
|
-
const latestRun = runs.filter((run) => run.issueId === issueId).sort(
|
|
2986
|
+
const latestRun = runs.filter((run) => run.issueId === issueId).sort(
|
|
2987
|
+
(left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()
|
|
2988
|
+
)[0];
|
|
3272
2989
|
if (latestRun?.runtimeSession?.exitClassification !== "convergence-detected" || latestRun.issueState !== issueState) {
|
|
3273
2990
|
return false;
|
|
3274
2991
|
}
|
|
3275
|
-
const convergedAtMs =
|
|
3276
|
-
|
|
2992
|
+
const convergedAtMs = parseTimestampMs(
|
|
2993
|
+
latestRun.completedAt ?? latestRun.updatedAt
|
|
2994
|
+
);
|
|
2995
|
+
const issueUpdatedAtMs = parseTimestampMs(issueUpdatedAt);
|
|
3277
2996
|
if (convergedAtMs === null || issueUpdatedAtMs === null) {
|
|
3278
2997
|
return true;
|
|
3279
2998
|
}
|
|
3280
2999
|
return issueUpdatedAtMs <= convergedAtMs;
|
|
3281
3000
|
}
|
|
3282
|
-
function resolveIssueBudgetSnapshot(runs, issueId) {
|
|
3283
|
-
const issueRuns = runs.filter((run) => run.issueId === issueId);
|
|
3284
|
-
const startedAtCandidates = issueRuns.map((run) => run.startedAt).filter((value) => typeof value === "string");
|
|
3285
|
-
return {
|
|
3286
|
-
cumulativeTurnCount: issueRuns.reduce((total, run) => total + resolvePersistedCumulativeTurnCount(run), 0),
|
|
3287
|
-
tokenUsage: issueRuns.reduce((total, run) => ({
|
|
3288
|
-
inputTokens: total.inputTokens + (run.tokenUsage?.inputTokens ?? 0),
|
|
3289
|
-
outputTokens: total.outputTokens + (run.tokenUsage?.outputTokens ?? 0),
|
|
3290
|
-
totalTokens: total.totalTokens + (run.tokenUsage?.totalTokens ?? 0)
|
|
3291
|
-
}), {
|
|
3292
|
-
inputTokens: 0,
|
|
3293
|
-
outputTokens: 0,
|
|
3294
|
-
totalTokens: 0
|
|
3295
|
-
}),
|
|
3296
|
-
sessionStartedAt: startedAtCandidates.sort((left, right) => left.localeCompare(right))[0] ?? null
|
|
3297
|
-
};
|
|
3298
|
-
}
|
|
3299
|
-
function isIssueBudgetExceeded(snapshot, now, env = process.env) {
|
|
3300
|
-
const globalMaxTurns = parsePositiveInteger(env.SYMPHONY_GLOBAL_MAX_TURNS ?? "") ?? DEFAULT_GLOBAL_MAX_TURNS;
|
|
3301
|
-
if (snapshot.cumulativeTurnCount >= globalMaxTurns) {
|
|
3302
|
-
return true;
|
|
3303
|
-
}
|
|
3304
|
-
const maxTokens = parsePositiveInteger(env.SYMPHONY_MAX_TOKENS ?? "") ?? DEFAULT_MAX_TOKENS;
|
|
3305
|
-
if (snapshot.tokenUsage.totalTokens >= maxTokens) {
|
|
3306
|
-
return true;
|
|
3307
|
-
}
|
|
3308
|
-
const sessionTimeoutMs = parsePositiveInteger(env.SYMPHONY_SESSION_TIMEOUT_MS ?? "");
|
|
3309
|
-
if (sessionTimeoutMs === null || snapshot.sessionStartedAt === null) {
|
|
3310
|
-
return false;
|
|
3311
|
-
}
|
|
3312
|
-
return now.getTime() - new Date(snapshot.sessionStartedAt).getTime() >= sessionTimeoutMs;
|
|
3313
|
-
}
|
|
3314
|
-
function parsePositiveInteger(value) {
|
|
3315
|
-
const parsed = Number(value);
|
|
3316
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
3317
|
-
return null;
|
|
3318
|
-
}
|
|
3319
|
-
return Math.floor(parsed);
|
|
3320
|
-
}
|
|
3321
3001
|
function resolveCumulativeTurnCount(run, turnCount) {
|
|
3322
3002
|
const carriedTotal = resolvePersistedCumulativeTurnCount(run);
|
|
3323
3003
|
if (turnCount === null) {
|
|
@@ -3360,7 +3040,10 @@ function resolveWorkerCommand() {
|
|
|
3360
3040
|
return `node ${fileURLToPath(workerUrl)}`;
|
|
3361
3041
|
} catch {
|
|
3362
3042
|
try {
|
|
3363
|
-
const bundledWorker = join3(
|
|
3043
|
+
const bundledWorker = join3(
|
|
3044
|
+
fileURLToPath(new URL(".", import.meta.url)),
|
|
3045
|
+
"worker-entry.js"
|
|
3046
|
+
);
|
|
3364
3047
|
return `node ${bundledWorker}`;
|
|
3365
3048
|
} catch {
|
|
3366
3049
|
return DEFAULT_WORKER_COMMAND;
|
|
@@ -3373,17 +3056,13 @@ function createStore(runtimeRoot = ".runtime", options = {}) {
|
|
|
3373
3056
|
function sortCandidatesForDispatch(candidates) {
|
|
3374
3057
|
return [...candidates].sort((a, b) => {
|
|
3375
3058
|
if (a.priority !== b.priority) {
|
|
3376
|
-
if (a.priority === null)
|
|
3377
|
-
|
|
3378
|
-
if (b.priority === null)
|
|
3379
|
-
return -1;
|
|
3059
|
+
if (a.priority === null) return 1;
|
|
3060
|
+
if (b.priority === null) return -1;
|
|
3380
3061
|
return a.priority - b.priority;
|
|
3381
3062
|
}
|
|
3382
3063
|
if (a.createdAt !== b.createdAt) {
|
|
3383
|
-
if (a.createdAt === null)
|
|
3384
|
-
|
|
3385
|
-
if (b.createdAt === null)
|
|
3386
|
-
return -1;
|
|
3064
|
+
if (a.createdAt === null) return 1;
|
|
3065
|
+
if (b.createdAt === null) return -1;
|
|
3387
3066
|
return a.createdAt < b.createdAt ? -1 : 1;
|
|
3388
3067
|
}
|
|
3389
3068
|
return a.identifier.localeCompare(b.identifier);
|
|
@@ -3424,8 +3103,8 @@ function buildLatestRunMapByIssueId(runs) {
|
|
|
3424
3103
|
latestRuns.set(run.issueId, run);
|
|
3425
3104
|
continue;
|
|
3426
3105
|
}
|
|
3427
|
-
const runUpdatedAtMs =
|
|
3428
|
-
const existingUpdatedAtMs =
|
|
3106
|
+
const runUpdatedAtMs = parseTimestampMs(run.updatedAt) ?? -Infinity;
|
|
3107
|
+
const existingUpdatedAtMs = parseTimestampMs(existing.updatedAt) ?? -Infinity;
|
|
3429
3108
|
if (runUpdatedAtMs > existingUpdatedAtMs) {
|
|
3430
3109
|
latestRuns.set(run.issueId, run);
|
|
3431
3110
|
}
|
|
@@ -3437,7 +3116,9 @@ function isIssueOrchestrationClaimed(state) {
|
|
|
3437
3116
|
}
|
|
3438
3117
|
function upsertIssueOrchestration(issueRecords, nextRecord) {
|
|
3439
3118
|
const existingRecord = issueRecords.find((record) => record.issueId === nextRecord.issueId) ?? null;
|
|
3440
|
-
const remaining = issueRecords.filter(
|
|
3119
|
+
const remaining = issueRecords.filter(
|
|
3120
|
+
(record) => record.issueId !== nextRecord.issueId
|
|
3121
|
+
);
|
|
3441
3122
|
return [
|
|
3442
3123
|
...remaining,
|
|
3443
3124
|
{
|
|
@@ -3448,19 +3129,21 @@ function upsertIssueOrchestration(issueRecords, nextRecord) {
|
|
|
3448
3129
|
];
|
|
3449
3130
|
}
|
|
3450
3131
|
function releaseIssueOrchestration(issueRecords, issueId, now) {
|
|
3451
|
-
return issueRecords.map(
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3132
|
+
return issueRecords.map(
|
|
3133
|
+
(record) => record.issueId === issueId ? {
|
|
3134
|
+
...record,
|
|
3135
|
+
state: "released",
|
|
3136
|
+
currentRunId: null,
|
|
3137
|
+
retryEntry: null,
|
|
3138
|
+
updatedAt: now.toISOString()
|
|
3139
|
+
} : record
|
|
3140
|
+
);
|
|
3458
3141
|
}
|
|
3459
3142
|
function isActiveRunStatus(status) {
|
|
3460
3143
|
return status === "pending" || status === "starting" || status === "running" || status === "retrying";
|
|
3461
3144
|
}
|
|
3462
3145
|
|
|
3463
|
-
// ../orchestrator/
|
|
3146
|
+
// ../orchestrator/src/lock.ts
|
|
3464
3147
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3465
3148
|
import { mkdir as mkdir4, open as open2, readFile as readFile4, rm as rm4 } from "fs/promises";
|
|
3466
3149
|
import { dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as resolve2 } from "path";
|
|
@@ -3498,14 +3181,18 @@ async function acquireProjectLock(input) {
|
|
|
3498
3181
|
if (existing.status === "invalid") {
|
|
3499
3182
|
invalidReadAttempts += 1;
|
|
3500
3183
|
if (invalidReadAttempts >= LOCK_READ_RETRY_LIMIT) {
|
|
3501
|
-
throw new Error(
|
|
3184
|
+
throw new Error(
|
|
3185
|
+
`Project "${input.projectId}" lock file is unreadable at "${lockPath}".`
|
|
3186
|
+
);
|
|
3502
3187
|
}
|
|
3503
3188
|
await delay(LOCK_READ_RETRY_DELAY_MS);
|
|
3504
3189
|
continue;
|
|
3505
3190
|
}
|
|
3506
3191
|
invalidReadAttempts = 0;
|
|
3507
3192
|
if ((input.isProcessRunning ?? isProcessRunning)(existing.record.pid)) {
|
|
3508
|
-
throw new Error(
|
|
3193
|
+
throw new Error(
|
|
3194
|
+
`Project "${input.projectId}" is already running (PID ${existing.record.pid}).`
|
|
3195
|
+
);
|
|
3509
3196
|
}
|
|
3510
3197
|
await rm4(lockPath, { force: true });
|
|
3511
3198
|
}
|
|
@@ -3544,7 +3231,9 @@ async function readProjectLock(lockPath) {
|
|
|
3544
3231
|
}
|
|
3545
3232
|
function assertValidProjectId(projectId) {
|
|
3546
3233
|
if (projectId.length === 0 || projectId === "." || projectId === ".." || projectId.includes("/") || projectId.includes("\\")) {
|
|
3547
|
-
throw new Error(
|
|
3234
|
+
throw new Error(
|
|
3235
|
+
`Invalid project ID "${projectId}". Project IDs must not contain path separators or traversal segments.`
|
|
3236
|
+
);
|
|
3548
3237
|
}
|
|
3549
3238
|
}
|
|
3550
3239
|
function resolveProjectLockPath(runtimeRoot, projectId) {
|
|
@@ -3553,7 +3242,9 @@ function resolveProjectLockPath(runtimeRoot, projectId) {
|
|
|
3553
3242
|
const projectDir = resolve2(store.projectDir(projectId));
|
|
3554
3243
|
const relativeProjectDir = relative2(projectsRoot, projectDir);
|
|
3555
3244
|
if (relativeProjectDir.length === 0 || relativeProjectDir.startsWith("..") || isAbsolute(relativeProjectDir)) {
|
|
3556
|
-
throw new Error(
|
|
3245
|
+
throw new Error(
|
|
3246
|
+
`Invalid project ID "${projectId}". Project lock path must stay within "${projectsRoot}".`
|
|
3247
|
+
);
|
|
3557
3248
|
}
|
|
3558
3249
|
return join4(projectDir, ".lock");
|
|
3559
3250
|
}
|
|
@@ -3581,15 +3272,24 @@ function isProcessRunning(pid) {
|
|
|
3581
3272
|
}
|
|
3582
3273
|
}
|
|
3583
3274
|
function isAlreadyExistsError2(error) {
|
|
3584
|
-
return Boolean(
|
|
3275
|
+
return Boolean(
|
|
3276
|
+
error && typeof error === "object" && "code" in error && error.code === "EEXIST"
|
|
3277
|
+
);
|
|
3585
3278
|
}
|
|
3586
3279
|
function isMissingFileError2(error) {
|
|
3587
|
-
return Boolean(
|
|
3280
|
+
return Boolean(
|
|
3281
|
+
error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
3282
|
+
);
|
|
3588
3283
|
}
|
|
3589
3284
|
|
|
3590
|
-
// ../orchestrator/
|
|
3285
|
+
// ../orchestrator/src/index.ts
|
|
3591
3286
|
import { pathToFileURL } from "url";
|
|
3592
3287
|
import { resolve as resolve3 } from "path";
|
|
3288
|
+
|
|
3289
|
+
// ../orchestrator/src/runtime-factory.ts
|
|
3290
|
+
import { join as join5 } from "path";
|
|
3291
|
+
|
|
3292
|
+
// ../orchestrator/src/index.ts
|
|
3593
3293
|
function resolveOrchestratorLogLevel(value) {
|
|
3594
3294
|
if (!value || value === "normal") {
|
|
3595
3295
|
return "normal";
|
|
@@ -3597,7 +3297,9 @@ function resolveOrchestratorLogLevel(value) {
|
|
|
3597
3297
|
if (value === "verbose") {
|
|
3598
3298
|
return "verbose";
|
|
3599
3299
|
}
|
|
3600
|
-
throw new Error(
|
|
3300
|
+
throw new Error(
|
|
3301
|
+
`Unsupported log level: ${value}. Supported values: normal, verbose.`
|
|
3302
|
+
);
|
|
3601
3303
|
}
|
|
3602
3304
|
async function runCli(argv, dependencies = {}) {
|
|
3603
3305
|
const [command = "run-once", ...args] = argv;
|
|
@@ -3607,8 +3309,12 @@ async function runCli(argv, dependencies = {}) {
|
|
|
3607
3309
|
}
|
|
3608
3310
|
const runtimeRoot = resolve3(parsed.runtimeRoot ?? ".runtime");
|
|
3609
3311
|
const stderr = dependencies.stderr ?? process.stderr;
|
|
3610
|
-
const eventsDir = resolveOptionalPath(
|
|
3611
|
-
|
|
3312
|
+
const eventsDir = resolveOptionalPath(
|
|
3313
|
+
parsed.eventsDir ?? process.env.SYMPHONY_EVENTS_DIR
|
|
3314
|
+
);
|
|
3315
|
+
const logLevel = resolveOrchestratorLogLevel(
|
|
3316
|
+
parsed.logLevel ?? process.env.SYMPHONY_LOG_LEVEL
|
|
3317
|
+
);
|
|
3612
3318
|
const service = await dependencies.createService?.(runtimeRoot, parsed.projectId, {
|
|
3613
3319
|
eventsDir,
|
|
3614
3320
|
logLevel,
|
|
@@ -3656,8 +3362,10 @@ async function runCli(argv, dependencies = {}) {
|
|
|
3656
3362
|
let exitCode = 0;
|
|
3657
3363
|
void cleanup().catch((error) => {
|
|
3658
3364
|
exitCode = 1;
|
|
3659
|
-
stderr.write(
|
|
3660
|
-
`)
|
|
3365
|
+
stderr.write(
|
|
3366
|
+
`Failed to shut down orchestrator after ${signal}: ${error instanceof Error ? error.message : String(error)}
|
|
3367
|
+
`
|
|
3368
|
+
);
|
|
3661
3369
|
}).finally(() => {
|
|
3662
3370
|
exitProcess(exitCode);
|
|
3663
3371
|
});
|
|
@@ -3784,8 +3492,10 @@ function resolveOptionalPath(value) {
|
|
|
3784
3492
|
}
|
|
3785
3493
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
3786
3494
|
main().catch((error) => {
|
|
3787
|
-
process.stderr.write(
|
|
3788
|
-
|
|
3495
|
+
process.stderr.write(
|
|
3496
|
+
`${error instanceof Error ? error.message : "Unknown error"}
|
|
3497
|
+
`
|
|
3498
|
+
);
|
|
3789
3499
|
process.exitCode = 1;
|
|
3790
3500
|
});
|
|
3791
3501
|
}
|