@gh-symphony/cli 0.0.20 → 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-EKKT5USP.js → chunk-KY6WKH66.js} +437 -101
- package/dist/{chunk-HZVDTAPS.js → chunk-MYVJ6HK4.js} +943 -1182
- package/dist/{chunk-M3IFVLQS.js → chunk-QEONJ5DZ.js} +978 -72
- package/dist/{chunk-3AWF54PI.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-UUVHS3ZR.js → project-25NQ4J4Y.js} +8 -6
- package/dist/{recover-5KQI7WH5.js → recover-L3MJHHDA.js} +4 -2
- package/dist/{repo-HDDE7OUI.js → repo-TDCWQR6P.js} +72 -14
- package/dist/{run-ETC5UTRA.js → run-XJQ6BF7U.js} +4 -2
- package/dist/{setup-VWB7RZUQ.js → setup-B2SVLW2R.js} +46 -8
- package/dist/{start-ENFLZUI6.js → start-I2CC7BLW.js} +6 -4
- package/dist/{upgrade-3YNF3VKY.js → upgrade-OJXPZRYE.js} +2 -2
- package/dist/{version-NUBTTOG7.js → version-TBDCTKDO.js} +1 -1
- package/dist/worker-entry.js +489 -690
- package/dist/{workflow-TBIFY5MO.js → workflow-BLJH2HC3.js} +176 -10
- package/package.json +3 -1
|
@@ -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,18 +660,17 @@ 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;
|
|
@@ -1392,7 +683,7 @@ var STUCK_WORKER_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
|
1392
683
|
function isUsableWorkflowResolution(resolution) {
|
|
1393
684
|
return resolution.isValid || resolution.usedLastKnownGood;
|
|
1394
685
|
}
|
|
1395
|
-
function
|
|
686
|
+
function parseTimestampMs(value) {
|
|
1396
687
|
if (!value) {
|
|
1397
688
|
return null;
|
|
1398
689
|
}
|
|
@@ -1410,9 +701,11 @@ function parseFiniteNumber(value) {
|
|
|
1410
701
|
return null;
|
|
1411
702
|
}
|
|
1412
703
|
var OrchestratorService = class {
|
|
1413
|
-
store
|
|
1414
|
-
|
|
1415
|
-
|
|
704
|
+
constructor(store, projectConfig, dependencies = {}) {
|
|
705
|
+
this.store = store;
|
|
706
|
+
this.projectConfig = projectConfig;
|
|
707
|
+
this.dependencies = dependencies;
|
|
708
|
+
}
|
|
1416
709
|
projectPollIntervals = /* @__PURE__ */ new Map();
|
|
1417
710
|
activeWorkerPids = /* @__PURE__ */ new Set();
|
|
1418
711
|
workerStderrBuffers = /* @__PURE__ */ new Map();
|
|
@@ -1427,23 +720,25 @@ var OrchestratorService = class {
|
|
|
1427
720
|
sleepResolver = null;
|
|
1428
721
|
reconcilePromise = Promise.resolve();
|
|
1429
722
|
reconcileRequested = false;
|
|
1430
|
-
constructor(store, projectConfig, dependencies = {}) {
|
|
1431
|
-
this.store = store;
|
|
1432
|
-
this.projectConfig = projectConfig;
|
|
1433
|
-
this.dependencies = dependencies;
|
|
1434
|
-
}
|
|
1435
723
|
async run(options = {}) {
|
|
1436
724
|
this.running = true;
|
|
1437
|
-
await this.runSerialized(
|
|
725
|
+
await this.runSerialized(
|
|
726
|
+
() => this.performStartupCleanup(this.createTrackerDependencies())
|
|
727
|
+
);
|
|
1438
728
|
while (this.running) {
|
|
1439
729
|
try {
|
|
1440
|
-
const snapshot = await this.runOnceInternal(
|
|
730
|
+
const snapshot = await this.runOnceInternal(
|
|
731
|
+
options.issueIdentifier,
|
|
732
|
+
this.createTrackerDependencies()
|
|
733
|
+
);
|
|
1441
734
|
await this.notifyTick(snapshot);
|
|
1442
735
|
} catch (error) {
|
|
1443
736
|
if (options.once) {
|
|
1444
737
|
throw error;
|
|
1445
738
|
}
|
|
1446
|
-
this.writeStderr(
|
|
739
|
+
this.writeStderr(
|
|
740
|
+
`[orchestrator] run loop failed for ${this.projectConfig.projectId}: ${this.formatErrorMessage(error)}`
|
|
741
|
+
);
|
|
1447
742
|
}
|
|
1448
743
|
if (options.once || !this.running) {
|
|
1449
744
|
return;
|
|
@@ -1452,20 +747,39 @@ var OrchestratorService = class {
|
|
|
1452
747
|
}
|
|
1453
748
|
}
|
|
1454
749
|
async runOnce(options = {}) {
|
|
1455
|
-
return this.runOnceInternal(
|
|
750
|
+
return this.runOnceInternal(
|
|
751
|
+
options.issueIdentifier,
|
|
752
|
+
this.createTrackerDependencies()
|
|
753
|
+
);
|
|
1456
754
|
}
|
|
1457
755
|
async status() {
|
|
1458
756
|
return this.store.loadProjectStatus(this.projectConfig.projectId);
|
|
1459
757
|
}
|
|
1460
758
|
async statusForIssue(issueIdentifier) {
|
|
1461
|
-
const issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
1462
|
-
|
|
759
|
+
const issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
760
|
+
this.projectConfig.projectId
|
|
761
|
+
);
|
|
762
|
+
const issueRecord = issueRecords.find(
|
|
763
|
+
(record) => record.identifier === issueIdentifier
|
|
764
|
+
);
|
|
1463
765
|
if (!issueRecord) {
|
|
1464
766
|
return null;
|
|
1465
767
|
}
|
|
1466
|
-
const currentRunCandidate = issueRecord.currentRunId ? await this.store.loadRun(
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
+
);
|
|
1469
783
|
const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
|
|
1470
784
|
const currentAttempt = currentRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
|
|
1471
785
|
return {
|
|
@@ -1502,7 +816,10 @@ var OrchestratorService = class {
|
|
|
1502
816
|
codex_session_logs: currentRun === null ? [] : [
|
|
1503
817
|
{
|
|
1504
818
|
label: "worker",
|
|
1505
|
-
path: join3(
|
|
819
|
+
path: join3(
|
|
820
|
+
this.store.runDir(currentRun.runId, currentRun.projectId),
|
|
821
|
+
"worker.log"
|
|
822
|
+
),
|
|
1506
823
|
url: null
|
|
1507
824
|
}
|
|
1508
825
|
]
|
|
@@ -1564,7 +881,9 @@ var OrchestratorService = class {
|
|
|
1564
881
|
if (this.dependencies.pollIntervalMs) {
|
|
1565
882
|
return this.dependencies.pollIntervalMs;
|
|
1566
883
|
}
|
|
1567
|
-
const configuredIntervals = [...this.projectPollIntervals.values()].filter(
|
|
884
|
+
const configuredIntervals = [...this.projectPollIntervals.values()].filter(
|
|
885
|
+
(value) => Number.isFinite(value) && value > 0
|
|
886
|
+
);
|
|
1568
887
|
return configuredIntervals.length ? Math.min(...configuredIntervals) : DEFAULT_POLL_INTERVAL_MS;
|
|
1569
888
|
}
|
|
1570
889
|
async reconcileProject(tenant, issueIdentifier, trackerDependencies = {}) {
|
|
@@ -1577,27 +896,57 @@ var OrchestratorService = class {
|
|
|
1577
896
|
let pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
1578
897
|
let rateLimits = null;
|
|
1579
898
|
let trackerRateLimits = null;
|
|
1580
|
-
let issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
1581
|
-
|
|
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
|
+
);
|
|
1582
905
|
const activeRuns = allRuns.filter((run) => isActiveRunStatus(run.status));
|
|
1583
906
|
for (const run of activeRuns) {
|
|
1584
|
-
const outcome = await this.reconcileRun(
|
|
907
|
+
const outcome = await this.reconcileRun(
|
|
908
|
+
tenant,
|
|
909
|
+
run,
|
|
910
|
+
issueRecords,
|
|
911
|
+
trackerDependencies
|
|
912
|
+
);
|
|
1585
913
|
issueRecords = outcome.issueRecords;
|
|
1586
914
|
if (outcome.recovered) {
|
|
1587
915
|
recovered += 1;
|
|
1588
916
|
}
|
|
1589
917
|
}
|
|
1590
|
-
const reconciledRuns = (await this.store.loadAllRuns()).filter(
|
|
1591
|
-
|
|
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
|
+
);
|
|
1592
924
|
rateLimits = resolveProjectRateLimits(reconciledRuns, []);
|
|
1593
925
|
try {
|
|
1594
926
|
pollIntervalMs = await this.loadProjectPollInterval(tenant);
|
|
1595
|
-
const currentActiveRuns = (await this.store.loadAllRuns()).filter(
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
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;
|
|
1599
946
|
const { candidates: actionableCandidates, lifecycle } = await this.resolveActionableCandidates(tenant, filteredIssues);
|
|
1600
|
-
const trackedIssuesByIdentifier = new Map(
|
|
947
|
+
const trackedIssuesByIdentifier = new Map(
|
|
948
|
+
syncedIssuesByIdentifier
|
|
949
|
+
);
|
|
1601
950
|
for (const issue of filteredIssues) {
|
|
1602
951
|
const existing = trackedIssuesByIdentifier.get(issue.identifier);
|
|
1603
952
|
trackedIssuesByIdentifier.set(issue.identifier, {
|
|
@@ -1618,17 +967,33 @@ var OrchestratorService = class {
|
|
|
1618
967
|
rateLimits: existing.rateLimits ?? issue.rateLimits ?? null
|
|
1619
968
|
});
|
|
1620
969
|
}
|
|
1621
|
-
rateLimits = resolveProjectRateLimits(
|
|
1622
|
-
|
|
970
|
+
rateLimits = resolveProjectRateLimits(
|
|
971
|
+
syncedActiveRuns,
|
|
972
|
+
trackedIssuesByIdentifier.values()
|
|
973
|
+
);
|
|
974
|
+
trackerRateLimits = resolveTrackerRateLimits(
|
|
975
|
+
trackedIssuesByIdentifier.values()
|
|
976
|
+
);
|
|
1623
977
|
const concurrency = await this.getProjectConcurrency(tenant);
|
|
1624
|
-
const currentlyActive = issueRecords.filter(
|
|
978
|
+
const currentlyActive = issueRecords.filter(
|
|
979
|
+
(record) => isIssueOrchestrationClaimed(record.state)
|
|
980
|
+
).length;
|
|
1625
981
|
const availableSlots = Math.max(0, concurrency - currentlyActive);
|
|
1626
|
-
const latestRunsByIssueId = buildLatestRunMapByIssueId(
|
|
982
|
+
const latestRunsByIssueId = buildLatestRunMapByIssueId(
|
|
983
|
+
projectRunsAfterReconcile
|
|
984
|
+
);
|
|
1627
985
|
const unscheduledCandidates = actionableCandidates.filter((issue) => {
|
|
1628
|
-
if (hasConvergenceLockedRun(
|
|
986
|
+
if (hasConvergenceLockedRun(
|
|
987
|
+
projectRunsAfterReconcile,
|
|
988
|
+
issue.id,
|
|
989
|
+
issue.state,
|
|
990
|
+
issue.updatedAt
|
|
991
|
+
)) {
|
|
1629
992
|
return false;
|
|
1630
993
|
}
|
|
1631
|
-
return !issueRecords.some(
|
|
994
|
+
return !issueRecords.some(
|
|
995
|
+
(record) => record.issueId === issue.id && isIssueOrchestrationClaimed(record.state)
|
|
996
|
+
);
|
|
1632
997
|
});
|
|
1633
998
|
const sortedCandidates = sortCandidatesForDispatch(unscheduledCandidates);
|
|
1634
999
|
const activeByState = /* @__PURE__ */ new Map();
|
|
@@ -1643,9 +1008,13 @@ var OrchestratorService = class {
|
|
|
1643
1008
|
if (this.shuttingDown) {
|
|
1644
1009
|
break;
|
|
1645
1010
|
}
|
|
1646
|
-
if (slotsRemaining <= 0)
|
|
1647
|
-
|
|
1648
|
-
|
|
1011
|
+
if (slotsRemaining <= 0) break;
|
|
1012
|
+
if (await this.isFailureRetrySuppressedIssue(
|
|
1013
|
+
tenant,
|
|
1014
|
+
issue,
|
|
1015
|
+
issueRecords,
|
|
1016
|
+
latestRunsByIssueId.get(issue.id) ?? null
|
|
1017
|
+
)) {
|
|
1649
1018
|
continue;
|
|
1650
1019
|
}
|
|
1651
1020
|
const stateLimit = maxConcurrentByState[issue.state];
|
|
@@ -1655,11 +1024,14 @@ var OrchestratorService = class {
|
|
|
1655
1024
|
continue;
|
|
1656
1025
|
}
|
|
1657
1026
|
}
|
|
1658
|
-
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1027
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1028
|
+
{
|
|
1029
|
+
projectId: tenant.projectId,
|
|
1030
|
+
adapter: issue.tracker.adapter,
|
|
1031
|
+
issueSubjectId: issue.id
|
|
1032
|
+
},
|
|
1033
|
+
issue.identifier
|
|
1034
|
+
);
|
|
1663
1035
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
1664
1036
|
issueId: issue.id,
|
|
1665
1037
|
identifier: issue.identifier,
|
|
@@ -1695,10 +1067,15 @@ var OrchestratorService = class {
|
|
|
1695
1067
|
issueId: run.issueId,
|
|
1696
1068
|
issueState: issue.state
|
|
1697
1069
|
});
|
|
1698
|
-
this.logVerbose(
|
|
1070
|
+
this.logVerbose(
|
|
1071
|
+
`[dispatch] Issue ${issue.identifier} \u2192 run ${run.runId}`
|
|
1072
|
+
);
|
|
1699
1073
|
dispatched += 1;
|
|
1700
1074
|
slotsRemaining -= 1;
|
|
1701
|
-
activeByState.set(
|
|
1075
|
+
activeByState.set(
|
|
1076
|
+
issue.state,
|
|
1077
|
+
(activeByState.get(issue.state) ?? 0) + 1
|
|
1078
|
+
);
|
|
1702
1079
|
}
|
|
1703
1080
|
for (const issueRecord of issueRecords) {
|
|
1704
1081
|
if (!isIssueOrchestrationClaimed(issueRecord.state)) {
|
|
@@ -1709,8 +1086,17 @@ var OrchestratorService = class {
|
|
|
1709
1086
|
continue;
|
|
1710
1087
|
}
|
|
1711
1088
|
const persistedRun = issueRecord.currentRunId ? await this.store.loadRun(issueRecord.currentRunId, tenant.projectId) : null;
|
|
1712
|
-
const activeRun = syncedActiveRuns.find(
|
|
1713
|
-
|
|
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
|
+
);
|
|
1714
1100
|
if (resolvedIssue) {
|
|
1715
1101
|
continue;
|
|
1716
1102
|
}
|
|
@@ -1729,9 +1115,15 @@ var OrchestratorService = class {
|
|
|
1729
1115
|
lastError: "Run suppressed because the tracker state is no longer actionable."
|
|
1730
1116
|
};
|
|
1731
1117
|
await this.store.saveRun(suppressedRun);
|
|
1732
|
-
this.logVerbose(
|
|
1118
|
+
this.logVerbose(
|
|
1119
|
+
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
1120
|
+
);
|
|
1733
1121
|
}
|
|
1734
|
-
issueRecords = releaseIssueOrchestration(
|
|
1122
|
+
issueRecords = releaseIssueOrchestration(
|
|
1123
|
+
issueRecords,
|
|
1124
|
+
issueRecord.issueId,
|
|
1125
|
+
now
|
|
1126
|
+
);
|
|
1735
1127
|
suppressed += 1;
|
|
1736
1128
|
}
|
|
1737
1129
|
const terminalIssuesByIdentifier = /* @__PURE__ */ new Map();
|
|
@@ -1747,14 +1139,28 @@ var OrchestratorService = class {
|
|
|
1747
1139
|
} catch (error) {
|
|
1748
1140
|
lastError = error instanceof Error ? error.message : "Unknown orchestration error";
|
|
1749
1141
|
}
|
|
1750
|
-
const effectivePollIntervalMs = resolveAdaptivePollIntervalMs(
|
|
1142
|
+
const effectivePollIntervalMs = resolveAdaptivePollIntervalMs(
|
|
1143
|
+
pollIntervalMs,
|
|
1144
|
+
trackerRateLimits
|
|
1145
|
+
);
|
|
1751
1146
|
if (effectivePollIntervalMs > pollIntervalMs && isLowRateLimit(trackerRateLimits, LOW_RATE_LIMIT_WARNING_THRESHOLD)) {
|
|
1752
|
-
this.writeStderr(
|
|
1147
|
+
this.writeStderr(
|
|
1148
|
+
`[orchestrator] low GitHub rate limit for ${tenant.projectId}: interval=${effectivePollIntervalMs}ms rateLimits=${JSON.stringify(
|
|
1149
|
+
trackerRateLimits
|
|
1150
|
+
)}`
|
|
1151
|
+
);
|
|
1753
1152
|
}
|
|
1754
1153
|
this.projectPollIntervals.set(tenant.projectId, effectivePollIntervalMs);
|
|
1755
|
-
await this.store.saveProjectIssueOrchestrations(
|
|
1756
|
-
|
|
1757
|
-
|
|
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
|
+
);
|
|
1758
1164
|
rateLimits = rateLimits ?? resolveProjectRateLimits(latestRuns, []);
|
|
1759
1165
|
const status = buildProjectSnapshot({
|
|
1760
1166
|
project: tenant,
|
|
@@ -1771,7 +1177,9 @@ var OrchestratorService = class {
|
|
|
1771
1177
|
async performStartupCleanup(trackerDependencies = {}) {
|
|
1772
1178
|
const tenant = this.projectConfig;
|
|
1773
1179
|
const now = this.now();
|
|
1774
|
-
const workspaceRecords = await this.store.loadIssueWorkspaces(
|
|
1180
|
+
const workspaceRecords = await this.store.loadIssueWorkspaces(
|
|
1181
|
+
tenant.projectId
|
|
1182
|
+
);
|
|
1775
1183
|
if (workspaceRecords.length === 0) {
|
|
1776
1184
|
return;
|
|
1777
1185
|
}
|
|
@@ -1779,10 +1187,20 @@ var OrchestratorService = class {
|
|
|
1779
1187
|
const workflowCache = /* @__PURE__ */ new Map();
|
|
1780
1188
|
let issues;
|
|
1781
1189
|
try {
|
|
1782
|
-
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
|
+
);
|
|
1783
1199
|
} catch (error) {
|
|
1784
1200
|
const message = error instanceof Error ? error.message : "Unknown tracker error";
|
|
1785
|
-
console.warn(
|
|
1201
|
+
console.warn(
|
|
1202
|
+
`[orchestrator] Startup cleanup skipped for project ${tenant.projectId}: ${message}`
|
|
1203
|
+
);
|
|
1786
1204
|
return;
|
|
1787
1205
|
}
|
|
1788
1206
|
const issuesById = new Map(issues.map((issue) => [issue.id, issue]));
|
|
@@ -1795,17 +1213,28 @@ var OrchestratorService = class {
|
|
|
1795
1213
|
continue;
|
|
1796
1214
|
}
|
|
1797
1215
|
try {
|
|
1798
|
-
const resolution = await this.loadStartupCleanupWorkflow(
|
|
1216
|
+
const resolution = await this.loadStartupCleanupWorkflow(
|
|
1217
|
+
tenant,
|
|
1218
|
+
issue.repository,
|
|
1219
|
+
workflowCache
|
|
1220
|
+
);
|
|
1799
1221
|
if (!resolution.isValid) {
|
|
1800
1222
|
continue;
|
|
1801
1223
|
}
|
|
1802
1224
|
if (!isStateTerminal(issue.state, resolution.lifecycle)) {
|
|
1803
1225
|
continue;
|
|
1804
1226
|
}
|
|
1805
|
-
await this.cleanupTerminalIssueWorkspace(
|
|
1227
|
+
await this.cleanupTerminalIssueWorkspace(
|
|
1228
|
+
tenant,
|
|
1229
|
+
issue,
|
|
1230
|
+
now,
|
|
1231
|
+
resolution
|
|
1232
|
+
);
|
|
1806
1233
|
} catch (error) {
|
|
1807
1234
|
const message = error instanceof Error ? error.message : "Unknown startup cleanup error";
|
|
1808
|
-
console.warn(
|
|
1235
|
+
console.warn(
|
|
1236
|
+
`[orchestrator] Startup cleanup skipped workspace for ${issue.identifier}: ${message}`
|
|
1237
|
+
);
|
|
1809
1238
|
}
|
|
1810
1239
|
}
|
|
1811
1240
|
}
|
|
@@ -1816,7 +1245,9 @@ var OrchestratorService = class {
|
|
|
1816
1245
|
try {
|
|
1817
1246
|
await this.dependencies.onTick(snapshot);
|
|
1818
1247
|
} catch (error) {
|
|
1819
|
-
this.writeStderr(
|
|
1248
|
+
this.writeStderr(
|
|
1249
|
+
`[orchestrator] onTick callback failed: ${this.formatErrorMessage(error)}`
|
|
1250
|
+
);
|
|
1820
1251
|
}
|
|
1821
1252
|
}
|
|
1822
1253
|
formatErrorMessage(error) {
|
|
@@ -1827,11 +1258,18 @@ var OrchestratorService = class {
|
|
|
1827
1258
|
}
|
|
1828
1259
|
async resolveStartupCleanupTerminalStates(tenant, workspaceRecords, workflowCache) {
|
|
1829
1260
|
const terminalStates = /* @__PURE__ */ new Map();
|
|
1830
|
-
const repositories = this.resolveStartupCleanupRepositories(
|
|
1261
|
+
const repositories = this.resolveStartupCleanupRepositories(
|
|
1262
|
+
tenant,
|
|
1263
|
+
workspaceRecords
|
|
1264
|
+
);
|
|
1831
1265
|
for (const repository of repositories) {
|
|
1832
1266
|
let resolution;
|
|
1833
1267
|
try {
|
|
1834
|
-
resolution = await this.loadStartupCleanupWorkflow(
|
|
1268
|
+
resolution = await this.loadStartupCleanupWorkflow(
|
|
1269
|
+
tenant,
|
|
1270
|
+
repository,
|
|
1271
|
+
workflowCache
|
|
1272
|
+
);
|
|
1835
1273
|
} catch {
|
|
1836
1274
|
continue;
|
|
1837
1275
|
}
|
|
@@ -1855,14 +1293,20 @@ var OrchestratorService = class {
|
|
|
1855
1293
|
resolveStartupCleanupRepositories(tenant, workspaceRecords) {
|
|
1856
1294
|
const repositories = /* @__PURE__ */ new Map();
|
|
1857
1295
|
for (const repository of tenant.repositories) {
|
|
1858
|
-
repositories.set(
|
|
1296
|
+
repositories.set(
|
|
1297
|
+
this.startupCleanupRepositoryKey(repository.owner, repository.name),
|
|
1298
|
+
repository
|
|
1299
|
+
);
|
|
1859
1300
|
}
|
|
1860
1301
|
for (const workspaceRecord of workspaceRecords) {
|
|
1861
1302
|
const repository = this.parseWorkspaceRepositoryRef(workspaceRecord);
|
|
1862
1303
|
if (!repository) {
|
|
1863
1304
|
continue;
|
|
1864
1305
|
}
|
|
1865
|
-
const key = this.startupCleanupRepositoryKey(
|
|
1306
|
+
const key = this.startupCleanupRepositoryKey(
|
|
1307
|
+
repository.owner,
|
|
1308
|
+
repository.name
|
|
1309
|
+
);
|
|
1866
1310
|
if (!repositories.has(key)) {
|
|
1867
1311
|
repositories.set(key, repository);
|
|
1868
1312
|
}
|
|
@@ -1870,7 +1314,9 @@ var OrchestratorService = class {
|
|
|
1870
1314
|
return [...repositories.values()];
|
|
1871
1315
|
}
|
|
1872
1316
|
parseWorkspaceRepositoryRef(workspaceRecord) {
|
|
1873
|
-
const match = workspaceRecord.issueIdentifier.match(
|
|
1317
|
+
const match = workspaceRecord.issueIdentifier.match(
|
|
1318
|
+
/^([^/]+)\/([^#]+)#\d+$/
|
|
1319
|
+
);
|
|
1874
1320
|
if (!match) {
|
|
1875
1321
|
return null;
|
|
1876
1322
|
}
|
|
@@ -1894,7 +1340,9 @@ var OrchestratorService = class {
|
|
|
1894
1340
|
if (cachedResolution) {
|
|
1895
1341
|
return cachedResolution;
|
|
1896
1342
|
}
|
|
1897
|
-
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);
|
|
1898
1346
|
workflowCache.set(cacheKey, resolutionPromise);
|
|
1899
1347
|
return resolutionPromise;
|
|
1900
1348
|
}
|
|
@@ -1916,7 +1364,11 @@ var OrchestratorService = class {
|
|
|
1916
1364
|
const workflowResolutionCache = /* @__PURE__ */ new Map();
|
|
1917
1365
|
this.workflowResolutionCache = workflowResolutionCache;
|
|
1918
1366
|
try {
|
|
1919
|
-
return await this.reconcileProject(
|
|
1367
|
+
return await this.reconcileProject(
|
|
1368
|
+
this.projectConfig,
|
|
1369
|
+
issueIdentifier,
|
|
1370
|
+
trackerDependencies
|
|
1371
|
+
);
|
|
1920
1372
|
} finally {
|
|
1921
1373
|
if (this.workflowResolutionCache === workflowResolutionCache) {
|
|
1922
1374
|
this.workflowResolutionCache = null;
|
|
@@ -1931,14 +1383,21 @@ var OrchestratorService = class {
|
|
|
1931
1383
|
};
|
|
1932
1384
|
}
|
|
1933
1385
|
async findLatestRunForIssue(issueId, issueIdentifier) {
|
|
1934
|
-
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
|
+
);
|
|
1935
1391
|
return matchingRuns[0] ?? null;
|
|
1936
1392
|
}
|
|
1937
1393
|
async resolveActionableCandidates(tenant, issues) {
|
|
1938
1394
|
const candidates = [];
|
|
1939
1395
|
let lifecycle = null;
|
|
1940
1396
|
for (const issue of issues) {
|
|
1941
|
-
const resolution = await this.loadProjectWorkflow(
|
|
1397
|
+
const resolution = await this.loadProjectWorkflow(
|
|
1398
|
+
tenant,
|
|
1399
|
+
issue.repository
|
|
1400
|
+
);
|
|
1942
1401
|
if (!isUsableWorkflowResolution(resolution)) {
|
|
1943
1402
|
continue;
|
|
1944
1403
|
}
|
|
@@ -1951,7 +1410,10 @@ var OrchestratorService = class {
|
|
|
1951
1410
|
candidates.push(issue);
|
|
1952
1411
|
}
|
|
1953
1412
|
if (!lifecycle && tenant.repositories.length > 0) {
|
|
1954
|
-
const resolution = await this.loadProjectWorkflow(
|
|
1413
|
+
const resolution = await this.loadProjectWorkflow(
|
|
1414
|
+
tenant,
|
|
1415
|
+
tenant.repositories[0]
|
|
1416
|
+
);
|
|
1955
1417
|
if (isUsableWorkflowResolution(resolution)) {
|
|
1956
1418
|
lifecycle = resolution.lifecycle;
|
|
1957
1419
|
}
|
|
@@ -1978,7 +1440,9 @@ var OrchestratorService = class {
|
|
|
1978
1440
|
return false;
|
|
1979
1441
|
}
|
|
1980
1442
|
if (blockerRef.identifier) {
|
|
1981
|
-
const blockerIssue = issues.find(
|
|
1443
|
+
const blockerIssue = issues.find(
|
|
1444
|
+
(candidate) => candidate.identifier === blockerRef.identifier
|
|
1445
|
+
);
|
|
1982
1446
|
if (blockerIssue?.state) {
|
|
1983
1447
|
return !isStateTerminal(blockerIssue.state, lifecycle);
|
|
1984
1448
|
}
|
|
@@ -1994,24 +1458,42 @@ var OrchestratorService = class {
|
|
|
1994
1458
|
if (cachedResolution) {
|
|
1995
1459
|
return cachedResolution;
|
|
1996
1460
|
}
|
|
1997
|
-
const resolutionPromise = this.loadProjectWorkflowUncached(
|
|
1461
|
+
const resolutionPromise = this.loadProjectWorkflowUncached(
|
|
1462
|
+
tenant,
|
|
1463
|
+
repository
|
|
1464
|
+
);
|
|
1998
1465
|
pendingCache.set(cacheKey, resolutionPromise);
|
|
1999
1466
|
return resolutionPromise;
|
|
2000
1467
|
}
|
|
2001
1468
|
return this.loadProjectWorkflowUncached(tenant, repository);
|
|
2002
1469
|
}
|
|
2003
1470
|
async loadProjectWorkflowUncached(tenant, repository) {
|
|
2004
|
-
const cacheRoot = join3(
|
|
1471
|
+
const cacheRoot = join3(
|
|
1472
|
+
this.store.projectDir(tenant.projectId),
|
|
1473
|
+
"cache",
|
|
1474
|
+
repository.owner,
|
|
1475
|
+
repository.name
|
|
1476
|
+
);
|
|
2005
1477
|
const { repositoryDirectory, changed } = await syncRepositoryForRun({
|
|
2006
1478
|
repository,
|
|
2007
1479
|
targetDirectory: cacheRoot
|
|
2008
1480
|
});
|
|
2009
|
-
const resolution = await loadRepositoryWorkflow(
|
|
2010
|
-
|
|
1481
|
+
const resolution = await loadRepositoryWorkflow(
|
|
1482
|
+
repositoryDirectory,
|
|
1483
|
+
repository
|
|
1484
|
+
);
|
|
1485
|
+
return this.resolveWorkflowResolution(
|
|
1486
|
+
repository,
|
|
1487
|
+
cacheRoot,
|
|
1488
|
+
resolution,
|
|
1489
|
+
changed
|
|
1490
|
+
);
|
|
2011
1491
|
}
|
|
2012
1492
|
async startRun(tenant, issue) {
|
|
2013
1493
|
if (this.shuttingDown || !this.running) {
|
|
2014
|
-
throw new Error(
|
|
1494
|
+
throw new Error(
|
|
1495
|
+
"Orchestrator is shutting down and cannot start new runs."
|
|
1496
|
+
);
|
|
2015
1497
|
}
|
|
2016
1498
|
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
2017
1499
|
const now = this.now();
|
|
@@ -2024,12 +1506,24 @@ var OrchestratorService = class {
|
|
|
2024
1506
|
adapter: issue.tracker.adapter,
|
|
2025
1507
|
issueSubjectId
|
|
2026
1508
|
};
|
|
2027
|
-
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1509
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1510
|
+
identity,
|
|
1511
|
+
issue.identifier
|
|
1512
|
+
);
|
|
2028
1513
|
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(identity);
|
|
2029
|
-
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
|
+
));
|
|
2030
1521
|
const workspaceKey = existingWorkspaceRecord?.workspaceKey ?? preferredWorkspaceKey;
|
|
2031
1522
|
const projectDir = this.store.projectDir(tenant.projectId);
|
|
2032
|
-
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
1523
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
1524
|
+
projectDir,
|
|
1525
|
+
workspaceKey
|
|
1526
|
+
);
|
|
2033
1527
|
const repositoryDirectory = await ensureIssueWorkspaceRepository({
|
|
2034
1528
|
repository: issue.repository,
|
|
2035
1529
|
issueWorkspacePath
|
|
@@ -2049,14 +1543,20 @@ var OrchestratorService = class {
|
|
|
2049
1543
|
lastError: null
|
|
2050
1544
|
};
|
|
2051
1545
|
await this.store.saveIssueWorkspace(workspaceRecord);
|
|
2052
|
-
const afterCreateResult = await this.runHook(
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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
|
+
);
|
|
2060
1560
|
if (afterCreateResult && afterCreateResult.outcome !== "success" && afterCreateResult.outcome !== "skipped") {
|
|
2061
1561
|
await this.store.appendRunEvent(runId, {
|
|
2062
1562
|
at: now.toISOString(),
|
|
@@ -2069,23 +1569,35 @@ var OrchestratorService = class {
|
|
|
2069
1569
|
}
|
|
2070
1570
|
const workflow = await this.loadProjectWorkflow(tenant, issue.repository);
|
|
2071
1571
|
if (!isUsableWorkflowResolution(workflow)) {
|
|
2072
|
-
throw new Error(
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
workflow.validationError ?? "Invalid repository WORKFLOW.md"
|
|
1574
|
+
);
|
|
2073
1575
|
}
|
|
2074
1576
|
const promptVariables = buildPromptVariables(issue, {
|
|
2075
1577
|
attempt: null
|
|
2076
1578
|
// first execution
|
|
2077
1579
|
});
|
|
2078
|
-
const renderedPrompt = renderPrompt(
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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);
|
|
2089
1601
|
mkdirSync(runDir, { recursive: true });
|
|
2090
1602
|
const workerLogStream = (this.dependencies.createWriteStreamImpl ?? createWriteStream)(join3(runDir, "worker.log"), {
|
|
2091
1603
|
flags: "a"
|
|
@@ -2108,57 +1620,69 @@ var OrchestratorService = class {
|
|
|
2108
1620
|
}
|
|
2109
1621
|
workerLogAvailable = false;
|
|
2110
1622
|
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2111
|
-
this.writeStderr(
|
|
1623
|
+
this.writeStderr(
|
|
1624
|
+
`[orchestrator] failed to write worker log for ${runId}: ${message}`
|
|
1625
|
+
);
|
|
2112
1626
|
};
|
|
2113
|
-
const child = (this.dependencies.spawnImpl ?? spawn2)(
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
-
|
|
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
|
+
);
|
|
2162
1686
|
const handleWorkerStderrChunk = (chunk) => {
|
|
2163
1687
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
2164
1688
|
if (workerLogAvailable) {
|
|
@@ -2195,7 +1719,9 @@ var OrchestratorService = class {
|
|
|
2195
1719
|
if (child.pid) {
|
|
2196
1720
|
this.retireWorkerPid(child.pid);
|
|
2197
1721
|
}
|
|
2198
|
-
this.logVerbose(
|
|
1722
|
+
this.logVerbose(
|
|
1723
|
+
`[worker-exited] ${runId} (code=${code ?? "null"}, signal=${signal ?? "null"})`
|
|
1724
|
+
);
|
|
2199
1725
|
};
|
|
2200
1726
|
const finalizeWorkerStderr = (code, signal) => {
|
|
2201
1727
|
if (workerExited || workerStderrFinalizing) {
|
|
@@ -2235,7 +1761,9 @@ var OrchestratorService = class {
|
|
|
2235
1761
|
}
|
|
2236
1762
|
child.on?.("error", (error) => {
|
|
2237
1763
|
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2238
|
-
this.writeStderr(
|
|
1764
|
+
this.writeStderr(
|
|
1765
|
+
`[orchestrator] worker process error for ${runId}: ${message}`
|
|
1766
|
+
);
|
|
2239
1767
|
finalizeWorkerStderr(null, null);
|
|
2240
1768
|
});
|
|
2241
1769
|
child.on?.("close", (code, signal) => {
|
|
@@ -2282,14 +1810,24 @@ var OrchestratorService = class {
|
|
|
2282
1810
|
issuesByIdentifier: /* @__PURE__ */ new Map()
|
|
2283
1811
|
};
|
|
2284
1812
|
}
|
|
2285
|
-
const issues = await trackerAdapter.fetchIssueStatesByIds(
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
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
|
+
);
|
|
2290
1826
|
const syncedRuns = [];
|
|
2291
1827
|
for (const run of activeRuns) {
|
|
2292
|
-
const currentTrackerState = issueStateByIdentifier.get(
|
|
1828
|
+
const currentTrackerState = issueStateByIdentifier.get(
|
|
1829
|
+
run.issueIdentifier
|
|
1830
|
+
);
|
|
2293
1831
|
if (!currentTrackerState || currentTrackerState === run.issueState) {
|
|
2294
1832
|
syncedRuns.push(run);
|
|
2295
1833
|
continue;
|
|
@@ -2312,8 +1850,10 @@ var OrchestratorService = class {
|
|
|
2312
1850
|
if (run.processId && this.isProcessRunning(run.processId)) {
|
|
2313
1851
|
const retryPolicy = await this.loadRetryPolicy(tenant, run.repository);
|
|
2314
1852
|
const configuredStallTimeoutMs = retryPolicy?.stallTimeoutMs ?? null;
|
|
2315
|
-
const lastActivityAtMs =
|
|
2316
|
-
|
|
1853
|
+
const lastActivityAtMs = parseTimestampMs(
|
|
1854
|
+
run.lastEventAt ?? run.startedAt
|
|
1855
|
+
);
|
|
1856
|
+
const startedAtMs = parseTimestampMs(run.startedAt);
|
|
2317
1857
|
const elapsedSinceLastActivityMs = lastActivityAtMs === null ? null : now.getTime() - lastActivityAtMs;
|
|
2318
1858
|
const runningSinceMs = startedAtMs === null ? null : now.getTime() - startedAtMs;
|
|
2319
1859
|
const isStalledByWorkflowTimeout = configuredStallTimeoutMs !== null && configuredStallTimeoutMs > 0 && elapsedSinceLastActivityMs !== null && elapsedSinceLastActivityMs > configuredStallTimeoutMs;
|
|
@@ -2324,9 +1864,13 @@ var OrchestratorService = class {
|
|
|
2324
1864
|
const elapsedSeconds = Math.round((elapsedMs ?? 0) / 1e3);
|
|
2325
1865
|
const timeoutSeconds = Math.round((timeoutMs ?? 0) / 1e3);
|
|
2326
1866
|
if (this.isVerboseLoggingEnabled()) {
|
|
2327
|
-
this.writeStderr(
|
|
1867
|
+
this.writeStderr(
|
|
1868
|
+
`[stall-detected] ${run.runId} (elapsed=${elapsedSeconds}s > ${timeoutSeconds}s)`
|
|
1869
|
+
);
|
|
2328
1870
|
} else {
|
|
2329
|
-
this.writeStderr(
|
|
1871
|
+
this.writeStderr(
|
|
1872
|
+
`[orchestrator] stuck worker detected for ${run.runId} (elapsed ${elapsedSeconds}s > ${timeoutSeconds}s) \u2014 sending SIGTERM`
|
|
1873
|
+
);
|
|
2330
1874
|
}
|
|
2331
1875
|
this.sendSignal(run.processId, "SIGTERM");
|
|
2332
1876
|
} else {
|
|
@@ -2339,11 +1883,14 @@ var OrchestratorService = class {
|
|
|
2339
1883
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2340
1884
|
issueId: run.issueId,
|
|
2341
1885
|
identifier: run.issueIdentifier,
|
|
2342
|
-
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
1886
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
1887
|
+
{
|
|
1888
|
+
projectId: tenant.projectId,
|
|
1889
|
+
adapter: tenant.tracker.adapter,
|
|
1890
|
+
issueSubjectId: run.issueSubjectId
|
|
1891
|
+
},
|
|
1892
|
+
run.issueIdentifier
|
|
1893
|
+
),
|
|
2347
1894
|
state: "running",
|
|
2348
1895
|
currentRunId: run.runId,
|
|
2349
1896
|
retryEntry: null,
|
|
@@ -2361,12 +1908,29 @@ var OrchestratorService = class {
|
|
|
2361
1908
|
const workerInfo = await this.fetchWorkerRunInfo(run);
|
|
2362
1909
|
const runWithTokens = {
|
|
2363
1910
|
...run,
|
|
2364
|
-
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
|
+
),
|
|
2365
1920
|
threadId: workerInfo.threadId ?? run.threadId ?? null,
|
|
2366
|
-
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
1921
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
1922
|
+
run,
|
|
1923
|
+
workerInfo.turnCount ?? null
|
|
1924
|
+
),
|
|
2367
1925
|
tokenUsage: workerInfo.tokenUsage ?? run.tokenUsage,
|
|
2368
1926
|
lastEvent: workerInfo.lastEvent ?? run.lastEvent,
|
|
2369
|
-
lastTurnSummary: resolveLastTurnSummary(
|
|
1927
|
+
lastTurnSummary: resolveLastTurnSummary(
|
|
1928
|
+
run.lastTurnSummary,
|
|
1929
|
+
resolveLastTurnSummaryCandidate(
|
|
1930
|
+
workerInfo.lastEvent,
|
|
1931
|
+
workerInfo.lastError
|
|
1932
|
+
)
|
|
1933
|
+
),
|
|
2370
1934
|
lastEventAt: workerInfo.lastEventAt ?? run.lastEventAt ?? void 0,
|
|
2371
1935
|
lastEventAtSource: workerInfo.lastEventAtSource ?? run.lastEventAtSource ?? void 0,
|
|
2372
1936
|
executionPhase: workerInfo.executionPhase ?? run.executionPhase ?? null,
|
|
@@ -2392,7 +1956,11 @@ var OrchestratorService = class {
|
|
|
2392
1956
|
recovered: false
|
|
2393
1957
|
};
|
|
2394
1958
|
}
|
|
2395
|
-
if (await this.resolveRetryRestartAction(
|
|
1959
|
+
if (await this.resolveRetryRestartAction(
|
|
1960
|
+
tenant,
|
|
1961
|
+
run,
|
|
1962
|
+
trackerDependencies
|
|
1963
|
+
) === "release") {
|
|
2396
1964
|
return this.releaseRetryingRun(runWithTokens, issueRecords, now);
|
|
2397
1965
|
}
|
|
2398
1966
|
return this.restartRun(tenant, run, issueRecords, now, workerSessionId);
|
|
@@ -2410,28 +1978,46 @@ var OrchestratorService = class {
|
|
|
2410
1978
|
runPhase: runWithTokens.runPhase ?? "failed"
|
|
2411
1979
|
};
|
|
2412
1980
|
await this.store.saveRun(completedRun);
|
|
2413
|
-
this.logVerbose(
|
|
1981
|
+
this.logVerbose(
|
|
1982
|
+
`[run-completed] ${completedRun.runId} status=${completedRun.status}`
|
|
1983
|
+
);
|
|
2414
1984
|
return {
|
|
2415
1985
|
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
2416
1986
|
recovered: false
|
|
2417
1987
|
};
|
|
2418
1988
|
}
|
|
2419
1989
|
if (run.issueWorkspaceKey) {
|
|
2420
|
-
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
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
|
+
);
|
|
2431
2010
|
}
|
|
2432
|
-
const retryKind = await this.classifyRetryKind(
|
|
2011
|
+
const retryKind = await this.classifyRetryKind(
|
|
2012
|
+
tenant,
|
|
2013
|
+
run,
|
|
2014
|
+
trackerDependencies
|
|
2015
|
+
);
|
|
2433
2016
|
const failureRetryCount = retryKind === "failure" ? (this.resolveFailureRetryCount(issueRecords, run.issueId) ?? 0) + 1 : this.resolveFailureRetryCount(issueRecords, run.issueId) ?? 0;
|
|
2434
|
-
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2017
|
+
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2018
|
+
tenant,
|
|
2019
|
+
run.repository
|
|
2020
|
+
);
|
|
2435
2021
|
if (retryKind === "failure" && failureRetryCount >= maxFailureRetries) {
|
|
2436
2022
|
const lastError = [
|
|
2437
2023
|
`Run suppressed: ${MAX_FAILURE_RETRIES_EXCEEDED_REASON}.`,
|
|
@@ -2458,16 +2044,21 @@ var OrchestratorService = class {
|
|
|
2458
2044
|
issueId: run.issueId,
|
|
2459
2045
|
reason: MAX_FAILURE_RETRIES_EXCEEDED_REASON
|
|
2460
2046
|
});
|
|
2461
|
-
this.logVerbose(
|
|
2047
|
+
this.logVerbose(
|
|
2048
|
+
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
2049
|
+
);
|
|
2462
2050
|
return {
|
|
2463
2051
|
issueRecords: upsertIssueOrchestration(issueRecords, {
|
|
2464
2052
|
issueId: run.issueId,
|
|
2465
2053
|
identifier: run.issueIdentifier,
|
|
2466
|
-
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2054
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2055
|
+
{
|
|
2056
|
+
projectId: tenant.projectId,
|
|
2057
|
+
adapter: tenant.tracker.adapter,
|
|
2058
|
+
issueSubjectId: run.issueSubjectId
|
|
2059
|
+
},
|
|
2060
|
+
run.issueIdentifier
|
|
2061
|
+
),
|
|
2471
2062
|
state: "released",
|
|
2472
2063
|
failureRetryCount,
|
|
2473
2064
|
currentRunId: null,
|
|
@@ -2479,7 +2070,9 @@ var OrchestratorService = class {
|
|
|
2479
2070
|
}
|
|
2480
2071
|
let nextRetryAt;
|
|
2481
2072
|
if (retryKind === "continuation") {
|
|
2482
|
-
nextRetryAt = new Date(
|
|
2073
|
+
nextRetryAt = new Date(
|
|
2074
|
+
now.getTime() + CONTINUATION_RETRY_DELAY_MS
|
|
2075
|
+
).toISOString();
|
|
2483
2076
|
} else {
|
|
2484
2077
|
const retryOptions = await this.loadRetryPolicy(tenant, run.repository);
|
|
2485
2078
|
const backoffMs = this.dependencies.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
@@ -2500,16 +2093,23 @@ var OrchestratorService = class {
|
|
|
2500
2093
|
lastError: retryKind === "continuation" ? null : "Worker process exited unexpectedly."
|
|
2501
2094
|
};
|
|
2502
2095
|
await this.store.saveRun(retryRecord);
|
|
2503
|
-
this.logVerbose(
|
|
2504
|
-
|
|
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
|
+
);
|
|
2505
2102
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2506
2103
|
issueId: run.issueId,
|
|
2507
2104
|
identifier: run.issueIdentifier,
|
|
2508
|
-
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2105
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2106
|
+
{
|
|
2107
|
+
projectId: tenant.projectId,
|
|
2108
|
+
adapter: tenant.tracker.adapter,
|
|
2109
|
+
issueSubjectId: run.issueSubjectId
|
|
2110
|
+
},
|
|
2111
|
+
run.issueIdentifier
|
|
2112
|
+
),
|
|
2513
2113
|
state: "retry_queued",
|
|
2514
2114
|
completedOnce: retryKind === "continuation" ? true : void 0,
|
|
2515
2115
|
failureRetryCount,
|
|
@@ -2568,9 +2168,13 @@ var OrchestratorService = class {
|
|
|
2568
2168
|
if (!isOrchestratorChannelEvent(parsed)) {
|
|
2569
2169
|
return;
|
|
2570
2170
|
}
|
|
2571
|
-
void this.runSerialized(
|
|
2171
|
+
void this.runSerialized(
|
|
2172
|
+
() => this.applyWorkerChannelEvent(runId, parsed)
|
|
2173
|
+
).catch((error) => {
|
|
2572
2174
|
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2573
|
-
this.writeStderr(
|
|
2175
|
+
this.writeStderr(
|
|
2176
|
+
`[orchestrator] failed to apply worker channel event for ${runId}: ${message}`
|
|
2177
|
+
);
|
|
2574
2178
|
});
|
|
2575
2179
|
} catch {
|
|
2576
2180
|
}
|
|
@@ -2587,15 +2191,29 @@ var OrchestratorService = class {
|
|
|
2587
2191
|
...run,
|
|
2588
2192
|
updatedAt: nowIso2,
|
|
2589
2193
|
lastEvent: "heartbeat",
|
|
2590
|
-
lastTurnSummary: resolveLastTurnSummary(
|
|
2194
|
+
lastTurnSummary: resolveLastTurnSummary(
|
|
2195
|
+
run.lastTurnSummary,
|
|
2196
|
+
event.lastError
|
|
2197
|
+
),
|
|
2591
2198
|
lastEventAt: persistedLastEventAt,
|
|
2592
2199
|
lastEventAtSource: event.lastEventAt != null ? "event-channel" : run.lastEventAtSource ?? null,
|
|
2593
2200
|
tokenUsage: event.tokenUsage,
|
|
2594
2201
|
rateLimits: event.rateLimits,
|
|
2595
|
-
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
|
+
),
|
|
2596
2211
|
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2597
2212
|
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
2598
|
-
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2213
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2214
|
+
run,
|
|
2215
|
+
event.sessionInfo?.turnCount ?? null
|
|
2216
|
+
),
|
|
2599
2217
|
executionPhase: event.executionPhase ?? run.executionPhase,
|
|
2600
2218
|
runPhase: event.runPhase ?? run.runPhase,
|
|
2601
2219
|
lastError: event.lastError
|
|
@@ -2656,15 +2274,29 @@ var OrchestratorService = class {
|
|
|
2656
2274
|
...run,
|
|
2657
2275
|
updatedAt: nowIso,
|
|
2658
2276
|
lastEvent: event.event ?? run.lastEvent ?? null,
|
|
2659
|
-
lastTurnSummary: resolveLastTurnSummary(
|
|
2277
|
+
lastTurnSummary: resolveLastTurnSummary(
|
|
2278
|
+
run.lastTurnSummary,
|
|
2279
|
+
resolveLastTurnSummaryCandidate(event.event, event.lastError)
|
|
2280
|
+
),
|
|
2660
2281
|
lastEventAt: event.lastEventAt,
|
|
2661
2282
|
lastEventAtSource: "event-channel",
|
|
2662
2283
|
tokenUsage: event.tokenUsage ?? run.tokenUsage,
|
|
2663
2284
|
rateLimits: event.rateLimits ?? run.rateLimits ?? null,
|
|
2664
|
-
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
|
+
),
|
|
2665
2294
|
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2666
2295
|
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
2667
|
-
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2296
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2297
|
+
run,
|
|
2298
|
+
event.sessionInfo?.turnCount ?? null
|
|
2299
|
+
),
|
|
2668
2300
|
executionPhase: event.executionPhase ?? run.executionPhase ?? null,
|
|
2669
2301
|
runPhase: event.runPhase ?? run.runPhase ?? null,
|
|
2670
2302
|
lastError: event.lastError ?? run.lastError
|
|
@@ -2729,7 +2361,11 @@ var OrchestratorService = class {
|
|
|
2729
2361
|
*/
|
|
2730
2362
|
async classifyRetryKind(tenant, run, trackerDependencies = {}) {
|
|
2731
2363
|
try {
|
|
2732
|
-
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2364
|
+
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2365
|
+
tenant,
|
|
2366
|
+
run.issueIdentifier,
|
|
2367
|
+
trackerDependencies
|
|
2368
|
+
);
|
|
2733
2369
|
if (!eligibleContext) {
|
|
2734
2370
|
return "failure";
|
|
2735
2371
|
}
|
|
@@ -2737,14 +2373,22 @@ var OrchestratorService = class {
|
|
|
2737
2373
|
if (!isUsableWorkflowResolution(resolution)) {
|
|
2738
2374
|
return "failure";
|
|
2739
2375
|
}
|
|
2740
|
-
return this.isIssueCandidateEligible(
|
|
2376
|
+
return this.isIssueCandidateEligible(
|
|
2377
|
+
eligibleContext.issue,
|
|
2378
|
+
resolution.lifecycle,
|
|
2379
|
+
eligibleContext.issues
|
|
2380
|
+
) ? "continuation" : "failure";
|
|
2741
2381
|
} catch {
|
|
2742
2382
|
return "failure";
|
|
2743
2383
|
}
|
|
2744
2384
|
}
|
|
2745
2385
|
async resolveRetryRestartAction(tenant, run, trackerDependencies = {}) {
|
|
2746
2386
|
try {
|
|
2747
|
-
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2387
|
+
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2388
|
+
tenant,
|
|
2389
|
+
run.issueIdentifier,
|
|
2390
|
+
trackerDependencies
|
|
2391
|
+
);
|
|
2748
2392
|
if (!eligibleContext) {
|
|
2749
2393
|
return "release";
|
|
2750
2394
|
}
|
|
@@ -2752,7 +2396,11 @@ var OrchestratorService = class {
|
|
|
2752
2396
|
if (!isUsableWorkflowResolution(resolution)) {
|
|
2753
2397
|
return "restart";
|
|
2754
2398
|
}
|
|
2755
|
-
return this.isIssueCandidateEligible(
|
|
2399
|
+
return this.isIssueCandidateEligible(
|
|
2400
|
+
eligibleContext.issue,
|
|
2401
|
+
resolution.lifecycle,
|
|
2402
|
+
eligibleContext.issues
|
|
2403
|
+
) ? "restart" : "release";
|
|
2756
2404
|
} catch {
|
|
2757
2405
|
return "restart";
|
|
2758
2406
|
}
|
|
@@ -2763,7 +2411,9 @@ var OrchestratorService = class {
|
|
|
2763
2411
|
fetchImpl: this.dependencies.fetchImpl,
|
|
2764
2412
|
...trackerDependencies
|
|
2765
2413
|
});
|
|
2766
|
-
const issue = issues.find(
|
|
2414
|
+
const issue = issues.find(
|
|
2415
|
+
(candidate) => candidate.identifier === issueIdentifier
|
|
2416
|
+
);
|
|
2767
2417
|
return issue ? { issue, issues } : null;
|
|
2768
2418
|
}
|
|
2769
2419
|
async fetchWorkerRunInfo(run) {
|
|
@@ -2787,12 +2437,20 @@ var OrchestratorService = class {
|
|
|
2787
2437
|
async readPersistedWorkerTokenUsage(run) {
|
|
2788
2438
|
const artifactPaths = [
|
|
2789
2439
|
join3(run.workspaceRuntimeDir, "token-usage.json"),
|
|
2790
|
-
join3(
|
|
2440
|
+
join3(
|
|
2441
|
+
run.workspaceRuntimeDir,
|
|
2442
|
+
".orchestrator",
|
|
2443
|
+
"runs",
|
|
2444
|
+
run.runId,
|
|
2445
|
+
"token-usage.json"
|
|
2446
|
+
)
|
|
2791
2447
|
];
|
|
2792
2448
|
for (const artifactPath of artifactPaths) {
|
|
2793
2449
|
try {
|
|
2794
2450
|
const raw = await readFile3(artifactPath, "utf8");
|
|
2795
|
-
const tokenUsage = JSON.parse(
|
|
2451
|
+
const tokenUsage = JSON.parse(
|
|
2452
|
+
raw
|
|
2453
|
+
);
|
|
2796
2454
|
if (hasTokenUsage(tokenUsage)) {
|
|
2797
2455
|
return tokenUsage;
|
|
2798
2456
|
}
|
|
@@ -2813,7 +2471,10 @@ var OrchestratorService = class {
|
|
|
2813
2471
|
if (!isUsableWorkflowResolution(workflowResolution)) {
|
|
2814
2472
|
return null;
|
|
2815
2473
|
}
|
|
2816
|
-
const hookEnv = this.buildProjectExecutionEnv(
|
|
2474
|
+
const hookEnv = this.buildProjectExecutionEnv(
|
|
2475
|
+
tenant.projectId,
|
|
2476
|
+
buildHookEnv(context)
|
|
2477
|
+
);
|
|
2817
2478
|
return executeWorkspaceHook({
|
|
2818
2479
|
kind,
|
|
2819
2480
|
hooks: workflowResolution.workflow.hooks,
|
|
@@ -2831,14 +2492,24 @@ var OrchestratorService = class {
|
|
|
2831
2492
|
return readEnvFile(envPath);
|
|
2832
2493
|
} catch (error) {
|
|
2833
2494
|
const message = error instanceof Error ? error.message : "Unknown error occurred.";
|
|
2834
|
-
(this.dependencies.stderr ?? process.stderr).write(
|
|
2835
|
-
`
|
|
2495
|
+
(this.dependencies.stderr ?? process.stderr).write(
|
|
2496
|
+
`[warn] Failed to load project env for ${projectId} from ${envPath}: ${message}
|
|
2497
|
+
`
|
|
2498
|
+
);
|
|
2836
2499
|
return {};
|
|
2837
2500
|
}
|
|
2838
2501
|
}
|
|
2839
2502
|
buildProjectExecutionEnv(projectId, env) {
|
|
2840
|
-
const inheritedEnv = Object.fromEntries(
|
|
2841
|
-
|
|
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
|
+
);
|
|
2842
2513
|
return {
|
|
2843
2514
|
...this.readProjectEnv(projectId),
|
|
2844
2515
|
...inheritedEnv,
|
|
@@ -2854,7 +2525,10 @@ var OrchestratorService = class {
|
|
|
2854
2525
|
lastError: "Superseded by recovered run."
|
|
2855
2526
|
};
|
|
2856
2527
|
await this.store.saveRun(supersededRecord);
|
|
2857
|
-
const issue = resolveTrackerAdapter2(tenant.tracker).reviveIssue(
|
|
2528
|
+
const issue = resolveTrackerAdapter2(tenant.tracker).reviveIssue(
|
|
2529
|
+
tenant,
|
|
2530
|
+
run
|
|
2531
|
+
);
|
|
2858
2532
|
const restarted = await this.startRun(tenant, issue);
|
|
2859
2533
|
const recoveredRecord = {
|
|
2860
2534
|
...restarted,
|
|
@@ -2880,11 +2554,14 @@ var OrchestratorService = class {
|
|
|
2880
2554
|
issueRecords: upsertIssueOrchestration(issueRecords, {
|
|
2881
2555
|
issueId: recoveredRecord.issueId,
|
|
2882
2556
|
identifier: recoveredRecord.issueIdentifier,
|
|
2883
|
-
workspaceKey: recoveredRecord.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2557
|
+
workspaceKey: recoveredRecord.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2558
|
+
{
|
|
2559
|
+
projectId: tenant.projectId,
|
|
2560
|
+
adapter: tenant.tracker.adapter,
|
|
2561
|
+
issueSubjectId: recoveredRecord.issueSubjectId
|
|
2562
|
+
},
|
|
2563
|
+
recoveredRecord.issueIdentifier
|
|
2564
|
+
),
|
|
2888
2565
|
state: "running",
|
|
2889
2566
|
currentRunId: recoveredRecord.runId,
|
|
2890
2567
|
retryEntry: null,
|
|
@@ -2905,34 +2582,40 @@ var OrchestratorService = class {
|
|
|
2905
2582
|
lastError: "Retry canceled because the tracker issue is no longer actionable."
|
|
2906
2583
|
};
|
|
2907
2584
|
await this.store.saveRun(suppressedRun);
|
|
2908
|
-
this.logVerbose(
|
|
2585
|
+
this.logVerbose(
|
|
2586
|
+
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
2587
|
+
);
|
|
2909
2588
|
return {
|
|
2910
2589
|
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
2911
2590
|
recovered: false
|
|
2912
2591
|
};
|
|
2913
2592
|
}
|
|
2914
2593
|
async loadProjectPollInterval(tenant) {
|
|
2915
|
-
const intervals = await Promise.all(
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
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
|
+
);
|
|
2920
2603
|
return validIntervals.length ? Math.min(...validIntervals) : DEFAULT_POLL_INTERVAL_MS;
|
|
2921
2604
|
}
|
|
2922
2605
|
async loadProjectMaxConcurrentByState(tenant) {
|
|
2923
2606
|
const result = {};
|
|
2924
|
-
const resolutions = await Promise.all(
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
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
|
+
);
|
|
2931
2616
|
for (const resolution of resolutions) {
|
|
2932
|
-
if (!resolution)
|
|
2933
|
-
|
|
2934
|
-
if (!isUsableWorkflowResolution(resolution))
|
|
2935
|
-
continue;
|
|
2617
|
+
if (!resolution) continue;
|
|
2618
|
+
if (!isUsableWorkflowResolution(resolution)) continue;
|
|
2936
2619
|
const stateLimits = resolution.workflow.agent.maxConcurrentAgentsByState;
|
|
2937
2620
|
for (const [state, limit] of Object.entries(stateLimits)) {
|
|
2938
2621
|
const existing = result[state];
|
|
@@ -2951,7 +2634,7 @@ var OrchestratorService = class {
|
|
|
2951
2634
|
return {
|
|
2952
2635
|
baseDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.retryBaseDelayMs,
|
|
2953
2636
|
maxDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.maxRetryBackoffMs,
|
|
2954
|
-
stallTimeoutMs: resolution.workflow.
|
|
2637
|
+
stallTimeoutMs: resolveWorkflowRuntimeTimeouts(resolution.workflow).stallTimeoutMs
|
|
2955
2638
|
};
|
|
2956
2639
|
} catch {
|
|
2957
2640
|
if (!this.dependencies.retryBackoffMs) {
|
|
@@ -2968,15 +2651,22 @@ var OrchestratorService = class {
|
|
|
2968
2651
|
if (this.dependencies.concurrency !== void 0) {
|
|
2969
2652
|
return this.dependencies.concurrency;
|
|
2970
2653
|
}
|
|
2971
|
-
const limits = await Promise.all(
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
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
|
+
);
|
|
2980
2670
|
return validLimits.length ? Math.min(...validLimits) : DEFAULT_CONCURRENCY;
|
|
2981
2671
|
}
|
|
2982
2672
|
async resolveWorkflowResolution(repository, cacheRoot, resolution, changed) {
|
|
@@ -2990,7 +2680,10 @@ var OrchestratorService = class {
|
|
|
2990
2680
|
};
|
|
2991
2681
|
let workflowPath = effectiveResolution.workflowPath;
|
|
2992
2682
|
try {
|
|
2993
|
-
workflowPath = await this.persistLastKnownGoodWorkflow(
|
|
2683
|
+
workflowPath = await this.persistLastKnownGoodWorkflow(
|
|
2684
|
+
cacheRoot,
|
|
2685
|
+
effectiveResolution
|
|
2686
|
+
) ?? effectiveResolution.workflowPath;
|
|
2994
2687
|
} catch {
|
|
2995
2688
|
workflowPath = effectiveResolution.workflowPath;
|
|
2996
2689
|
}
|
|
@@ -3005,8 +2698,10 @@ var OrchestratorService = class {
|
|
|
3005
2698
|
const message = resolution.validationError ?? "Invalid repository WORKFLOW.md";
|
|
3006
2699
|
const previousMessage = this.lastReportedWorkflowErrors.get(cacheKey);
|
|
3007
2700
|
if (changed || previousMessage !== message) {
|
|
3008
|
-
process.stderr.write(
|
|
3009
|
-
`
|
|
2701
|
+
process.stderr.write(
|
|
2702
|
+
`[orchestrator] failed to reload WORKFLOW.md for ${repository.owner}/${repository.name}: ${message}
|
|
2703
|
+
`
|
|
2704
|
+
);
|
|
3010
2705
|
this.lastReportedWorkflowErrors.set(cacheKey, message);
|
|
3011
2706
|
}
|
|
3012
2707
|
if (!cached) {
|
|
@@ -3096,10 +2791,22 @@ var OrchestratorService = class {
|
|
|
3096
2791
|
adapter: issue.tracker.adapter,
|
|
3097
2792
|
issueSubjectId
|
|
3098
2793
|
};
|
|
3099
|
-
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
2794
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
2795
|
+
identity,
|
|
2796
|
+
issue.identifier
|
|
2797
|
+
);
|
|
3100
2798
|
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(identity);
|
|
3101
2799
|
const orchestrationRecord = (await this.store.loadProjectIssueOrchestrations(tenant.projectId)).find((record) => record.issueId === issue.id);
|
|
3102
|
-
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
|
+
));
|
|
3103
2810
|
if (!workspaceRecord || workspaceRecord.status === "removed") {
|
|
3104
2811
|
return;
|
|
3105
2812
|
}
|
|
@@ -3109,17 +2816,26 @@ var OrchestratorService = class {
|
|
|
3109
2816
|
updatedAt: now.toISOString()
|
|
3110
2817
|
};
|
|
3111
2818
|
await this.store.saveIssueWorkspace(pendingRecord);
|
|
3112
|
-
const hookResult = await this.runHook(
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
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
|
+
);
|
|
3120
2834
|
if (hookResult && hookResult.outcome !== "success" && hookResult.outcome !== "skipped") {
|
|
3121
2835
|
const errorMessage = hookResult.error ?? `before_remove hook ${hookResult.outcome}`;
|
|
3122
|
-
console.warn(
|
|
2836
|
+
console.warn(
|
|
2837
|
+
`[orchestrator] before_remove hook failed for ${issue.identifier}; continuing cleanup: ${errorMessage}`
|
|
2838
|
+
);
|
|
3123
2839
|
}
|
|
3124
2840
|
try {
|
|
3125
2841
|
await rm3(workspaceRecord.workspacePath, { recursive: true, force: true });
|
|
@@ -3137,19 +2853,26 @@ var OrchestratorService = class {
|
|
|
3137
2853
|
return issueRecords.find((record) => record.issueId === issueId)?.failureRetryCount ?? null;
|
|
3138
2854
|
}
|
|
3139
2855
|
async isFailureRetrySuppressedIssue(tenant, issue, issueRecords, latestRun) {
|
|
3140
|
-
const issueRecord = issueRecords.find(
|
|
2856
|
+
const issueRecord = issueRecords.find(
|
|
2857
|
+
(record) => record.issueId === issue.id || record.identifier === issue.identifier
|
|
2858
|
+
) ?? null;
|
|
3141
2859
|
if (!issueRecord || issueRecord.failureRetryCount <= 0) {
|
|
3142
2860
|
return false;
|
|
3143
2861
|
}
|
|
3144
|
-
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2862
|
+
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2863
|
+
tenant,
|
|
2864
|
+
issue.repository
|
|
2865
|
+
);
|
|
3145
2866
|
if (issueRecord.failureRetryCount < maxFailureRetries) {
|
|
3146
2867
|
return false;
|
|
3147
2868
|
}
|
|
3148
2869
|
if (!latestRun || latestRun.status !== "suppressed" || latestRun.issueState !== issue.state || !latestRun.lastError?.includes(MAX_FAILURE_RETRIES_EXCEEDED_REASON)) {
|
|
3149
2870
|
return false;
|
|
3150
2871
|
}
|
|
3151
|
-
const issueUpdatedAtMs =
|
|
3152
|
-
const suppressedAtMs =
|
|
2872
|
+
const issueUpdatedAtMs = parseTimestampMs(issue.updatedAt);
|
|
2873
|
+
const suppressedAtMs = parseTimestampMs(
|
|
2874
|
+
latestRun.completedAt ?? latestRun.updatedAt
|
|
2875
|
+
);
|
|
3153
2876
|
if (issueUpdatedAtMs === null || suppressedAtMs === null) {
|
|
3154
2877
|
return true;
|
|
3155
2878
|
}
|
|
@@ -3165,7 +2888,9 @@ var OrchestratorService = class {
|
|
|
3165
2888
|
}
|
|
3166
2889
|
};
|
|
3167
2890
|
function hasTokenUsage(tokenUsage) {
|
|
3168
|
-
return Boolean(
|
|
2891
|
+
return Boolean(
|
|
2892
|
+
tokenUsage && (tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0 || tokenUsage.totalTokens > 0)
|
|
2893
|
+
);
|
|
3169
2894
|
}
|
|
3170
2895
|
function isRecord(value) {
|
|
3171
2896
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -3177,7 +2902,9 @@ function resolveProjectRateLimits(runs, issues) {
|
|
|
3177
2902
|
if (!isRecord(run.rateLimits)) {
|
|
3178
2903
|
continue;
|
|
3179
2904
|
}
|
|
3180
|
-
const timestamp =
|
|
2905
|
+
const timestamp = parseTimestampMs(
|
|
2906
|
+
run.lastEventAt ?? run.updatedAt ?? run.startedAt
|
|
2907
|
+
);
|
|
3181
2908
|
const sortableTimestamp = timestamp ?? -Infinity;
|
|
3182
2909
|
if (sortableTimestamp >= latestRunTimestamp) {
|
|
3183
2910
|
latestRunTimestamp = sortableTimestamp;
|
|
@@ -3256,12 +2983,16 @@ function resolvePersistedCumulativeTurnCount(run) {
|
|
|
3256
2983
|
return run.cumulativeTurnCount ?? run.turnCount ?? 0;
|
|
3257
2984
|
}
|
|
3258
2985
|
function hasConvergenceLockedRun(runs, issueId, issueState, issueUpdatedAt) {
|
|
3259
|
-
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];
|
|
3260
2989
|
if (latestRun?.runtimeSession?.exitClassification !== "convergence-detected" || latestRun.issueState !== issueState) {
|
|
3261
2990
|
return false;
|
|
3262
2991
|
}
|
|
3263
|
-
const convergedAtMs =
|
|
3264
|
-
|
|
2992
|
+
const convergedAtMs = parseTimestampMs(
|
|
2993
|
+
latestRun.completedAt ?? latestRun.updatedAt
|
|
2994
|
+
);
|
|
2995
|
+
const issueUpdatedAtMs = parseTimestampMs(issueUpdatedAt);
|
|
3265
2996
|
if (convergedAtMs === null || issueUpdatedAtMs === null) {
|
|
3266
2997
|
return true;
|
|
3267
2998
|
}
|
|
@@ -3309,7 +3040,10 @@ function resolveWorkerCommand() {
|
|
|
3309
3040
|
return `node ${fileURLToPath(workerUrl)}`;
|
|
3310
3041
|
} catch {
|
|
3311
3042
|
try {
|
|
3312
|
-
const bundledWorker = join3(
|
|
3043
|
+
const bundledWorker = join3(
|
|
3044
|
+
fileURLToPath(new URL(".", import.meta.url)),
|
|
3045
|
+
"worker-entry.js"
|
|
3046
|
+
);
|
|
3313
3047
|
return `node ${bundledWorker}`;
|
|
3314
3048
|
} catch {
|
|
3315
3049
|
return DEFAULT_WORKER_COMMAND;
|
|
@@ -3322,17 +3056,13 @@ function createStore(runtimeRoot = ".runtime", options = {}) {
|
|
|
3322
3056
|
function sortCandidatesForDispatch(candidates) {
|
|
3323
3057
|
return [...candidates].sort((a, b) => {
|
|
3324
3058
|
if (a.priority !== b.priority) {
|
|
3325
|
-
if (a.priority === null)
|
|
3326
|
-
|
|
3327
|
-
if (b.priority === null)
|
|
3328
|
-
return -1;
|
|
3059
|
+
if (a.priority === null) return 1;
|
|
3060
|
+
if (b.priority === null) return -1;
|
|
3329
3061
|
return a.priority - b.priority;
|
|
3330
3062
|
}
|
|
3331
3063
|
if (a.createdAt !== b.createdAt) {
|
|
3332
|
-
if (a.createdAt === null)
|
|
3333
|
-
|
|
3334
|
-
if (b.createdAt === null)
|
|
3335
|
-
return -1;
|
|
3064
|
+
if (a.createdAt === null) return 1;
|
|
3065
|
+
if (b.createdAt === null) return -1;
|
|
3336
3066
|
return a.createdAt < b.createdAt ? -1 : 1;
|
|
3337
3067
|
}
|
|
3338
3068
|
return a.identifier.localeCompare(b.identifier);
|
|
@@ -3373,8 +3103,8 @@ function buildLatestRunMapByIssueId(runs) {
|
|
|
3373
3103
|
latestRuns.set(run.issueId, run);
|
|
3374
3104
|
continue;
|
|
3375
3105
|
}
|
|
3376
|
-
const runUpdatedAtMs =
|
|
3377
|
-
const existingUpdatedAtMs =
|
|
3106
|
+
const runUpdatedAtMs = parseTimestampMs(run.updatedAt) ?? -Infinity;
|
|
3107
|
+
const existingUpdatedAtMs = parseTimestampMs(existing.updatedAt) ?? -Infinity;
|
|
3378
3108
|
if (runUpdatedAtMs > existingUpdatedAtMs) {
|
|
3379
3109
|
latestRuns.set(run.issueId, run);
|
|
3380
3110
|
}
|
|
@@ -3386,7 +3116,9 @@ function isIssueOrchestrationClaimed(state) {
|
|
|
3386
3116
|
}
|
|
3387
3117
|
function upsertIssueOrchestration(issueRecords, nextRecord) {
|
|
3388
3118
|
const existingRecord = issueRecords.find((record) => record.issueId === nextRecord.issueId) ?? null;
|
|
3389
|
-
const remaining = issueRecords.filter(
|
|
3119
|
+
const remaining = issueRecords.filter(
|
|
3120
|
+
(record) => record.issueId !== nextRecord.issueId
|
|
3121
|
+
);
|
|
3390
3122
|
return [
|
|
3391
3123
|
...remaining,
|
|
3392
3124
|
{
|
|
@@ -3397,19 +3129,21 @@ function upsertIssueOrchestration(issueRecords, nextRecord) {
|
|
|
3397
3129
|
];
|
|
3398
3130
|
}
|
|
3399
3131
|
function releaseIssueOrchestration(issueRecords, issueId, now) {
|
|
3400
|
-
return issueRecords.map(
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
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
|
+
);
|
|
3407
3141
|
}
|
|
3408
3142
|
function isActiveRunStatus(status) {
|
|
3409
3143
|
return status === "pending" || status === "starting" || status === "running" || status === "retrying";
|
|
3410
3144
|
}
|
|
3411
3145
|
|
|
3412
|
-
// ../orchestrator/
|
|
3146
|
+
// ../orchestrator/src/lock.ts
|
|
3413
3147
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3414
3148
|
import { mkdir as mkdir4, open as open2, readFile as readFile4, rm as rm4 } from "fs/promises";
|
|
3415
3149
|
import { dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as resolve2 } from "path";
|
|
@@ -3447,14 +3181,18 @@ async function acquireProjectLock(input) {
|
|
|
3447
3181
|
if (existing.status === "invalid") {
|
|
3448
3182
|
invalidReadAttempts += 1;
|
|
3449
3183
|
if (invalidReadAttempts >= LOCK_READ_RETRY_LIMIT) {
|
|
3450
|
-
throw new Error(
|
|
3184
|
+
throw new Error(
|
|
3185
|
+
`Project "${input.projectId}" lock file is unreadable at "${lockPath}".`
|
|
3186
|
+
);
|
|
3451
3187
|
}
|
|
3452
3188
|
await delay(LOCK_READ_RETRY_DELAY_MS);
|
|
3453
3189
|
continue;
|
|
3454
3190
|
}
|
|
3455
3191
|
invalidReadAttempts = 0;
|
|
3456
3192
|
if ((input.isProcessRunning ?? isProcessRunning)(existing.record.pid)) {
|
|
3457
|
-
throw new Error(
|
|
3193
|
+
throw new Error(
|
|
3194
|
+
`Project "${input.projectId}" is already running (PID ${existing.record.pid}).`
|
|
3195
|
+
);
|
|
3458
3196
|
}
|
|
3459
3197
|
await rm4(lockPath, { force: true });
|
|
3460
3198
|
}
|
|
@@ -3493,7 +3231,9 @@ async function readProjectLock(lockPath) {
|
|
|
3493
3231
|
}
|
|
3494
3232
|
function assertValidProjectId(projectId) {
|
|
3495
3233
|
if (projectId.length === 0 || projectId === "." || projectId === ".." || projectId.includes("/") || projectId.includes("\\")) {
|
|
3496
|
-
throw new Error(
|
|
3234
|
+
throw new Error(
|
|
3235
|
+
`Invalid project ID "${projectId}". Project IDs must not contain path separators or traversal segments.`
|
|
3236
|
+
);
|
|
3497
3237
|
}
|
|
3498
3238
|
}
|
|
3499
3239
|
function resolveProjectLockPath(runtimeRoot, projectId) {
|
|
@@ -3502,7 +3242,9 @@ function resolveProjectLockPath(runtimeRoot, projectId) {
|
|
|
3502
3242
|
const projectDir = resolve2(store.projectDir(projectId));
|
|
3503
3243
|
const relativeProjectDir = relative2(projectsRoot, projectDir);
|
|
3504
3244
|
if (relativeProjectDir.length === 0 || relativeProjectDir.startsWith("..") || isAbsolute(relativeProjectDir)) {
|
|
3505
|
-
throw new Error(
|
|
3245
|
+
throw new Error(
|
|
3246
|
+
`Invalid project ID "${projectId}". Project lock path must stay within "${projectsRoot}".`
|
|
3247
|
+
);
|
|
3506
3248
|
}
|
|
3507
3249
|
return join4(projectDir, ".lock");
|
|
3508
3250
|
}
|
|
@@ -3530,15 +3272,24 @@ function isProcessRunning(pid) {
|
|
|
3530
3272
|
}
|
|
3531
3273
|
}
|
|
3532
3274
|
function isAlreadyExistsError2(error) {
|
|
3533
|
-
return Boolean(
|
|
3275
|
+
return Boolean(
|
|
3276
|
+
error && typeof error === "object" && "code" in error && error.code === "EEXIST"
|
|
3277
|
+
);
|
|
3534
3278
|
}
|
|
3535
3279
|
function isMissingFileError2(error) {
|
|
3536
|
-
return Boolean(
|
|
3280
|
+
return Boolean(
|
|
3281
|
+
error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
3282
|
+
);
|
|
3537
3283
|
}
|
|
3538
3284
|
|
|
3539
|
-
// ../orchestrator/
|
|
3285
|
+
// ../orchestrator/src/index.ts
|
|
3540
3286
|
import { pathToFileURL } from "url";
|
|
3541
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
|
|
3542
3293
|
function resolveOrchestratorLogLevel(value) {
|
|
3543
3294
|
if (!value || value === "normal") {
|
|
3544
3295
|
return "normal";
|
|
@@ -3546,7 +3297,9 @@ function resolveOrchestratorLogLevel(value) {
|
|
|
3546
3297
|
if (value === "verbose") {
|
|
3547
3298
|
return "verbose";
|
|
3548
3299
|
}
|
|
3549
|
-
throw new Error(
|
|
3300
|
+
throw new Error(
|
|
3301
|
+
`Unsupported log level: ${value}. Supported values: normal, verbose.`
|
|
3302
|
+
);
|
|
3550
3303
|
}
|
|
3551
3304
|
async function runCli(argv, dependencies = {}) {
|
|
3552
3305
|
const [command = "run-once", ...args] = argv;
|
|
@@ -3556,8 +3309,12 @@ async function runCli(argv, dependencies = {}) {
|
|
|
3556
3309
|
}
|
|
3557
3310
|
const runtimeRoot = resolve3(parsed.runtimeRoot ?? ".runtime");
|
|
3558
3311
|
const stderr = dependencies.stderr ?? process.stderr;
|
|
3559
|
-
const eventsDir = resolveOptionalPath(
|
|
3560
|
-
|
|
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
|
+
);
|
|
3561
3318
|
const service = await dependencies.createService?.(runtimeRoot, parsed.projectId, {
|
|
3562
3319
|
eventsDir,
|
|
3563
3320
|
logLevel,
|
|
@@ -3605,8 +3362,10 @@ async function runCli(argv, dependencies = {}) {
|
|
|
3605
3362
|
let exitCode = 0;
|
|
3606
3363
|
void cleanup().catch((error) => {
|
|
3607
3364
|
exitCode = 1;
|
|
3608
|
-
stderr.write(
|
|
3609
|
-
`)
|
|
3365
|
+
stderr.write(
|
|
3366
|
+
`Failed to shut down orchestrator after ${signal}: ${error instanceof Error ? error.message : String(error)}
|
|
3367
|
+
`
|
|
3368
|
+
);
|
|
3610
3369
|
}).finally(() => {
|
|
3611
3370
|
exitProcess(exitCode);
|
|
3612
3371
|
});
|
|
@@ -3733,8 +3492,10 @@ function resolveOptionalPath(value) {
|
|
|
3733
3492
|
}
|
|
3734
3493
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
3735
3494
|
main().catch((error) => {
|
|
3736
|
-
process.stderr.write(
|
|
3737
|
-
|
|
3495
|
+
process.stderr.write(
|
|
3496
|
+
`${error instanceof Error ? error.message : "Unknown error"}
|
|
3497
|
+
`
|
|
3498
|
+
);
|
|
3738
3499
|
process.exitCode = 1;
|
|
3739
3500
|
});
|
|
3740
3501
|
}
|