omp-conductor 0.4.3 → 0.4.4
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 +1 -1
- package/package.json +1 -1
- package/src/daemon.ts +48 -7
- package/src/store.ts +11 -1
- package/src/tracker/github.ts +26 -9
- package/src/types.ts +8 -4
package/README.md
CHANGED
|
@@ -1382,7 +1382,7 @@ persists the class on the row, and performs the one recovery that class names.
|
|
|
1382
1382
|
| `orphan-dirty` | orphaned with a failed salvage and no operator ack | hold — recorded only; the tree is the only copy | none |
|
|
1383
1383
|
| `orphan-clean` | orphaned with nothing uncommitted | requeue | continuation |
|
|
1384
1384
|
| `turn-cap-progress` | at the turn ceiling **with** a PR, head or salvage commit | continue from the branch | continuation |
|
|
1385
|
-
| `turn-cap-spinning` | at the ceiling with no PR and no commits | escalate with the last tool calls the transcript recorded | none |
|
|
1385
|
+
| `turn-cap-spinning` | at the ceiling with no PR and no commits | escalate with the last tool calls the transcript recorded — and the completion path deliberately does **not** requeue it | none |
|
|
1386
1386
|
| `admin-kill` | killed *below* its own ceiling — a restart or a drain | requeue | none |
|
|
1387
1387
|
| `ci-infra` | PR open, every unresolved check cancelled / timed out / stale | re-run the failed jobs | none |
|
|
1388
1388
|
| `ci-deterministic` | PR open, a check genuinely reports `FAILURE` | escalate with the failing check names and links | failed attempt |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
|
package/src/daemon.ts
CHANGED
|
@@ -903,6 +903,38 @@ export async function collectSettlementFlags(
|
|
|
903
903
|
* One attempt at one issue, from claim to terminal state. Everything is inside
|
|
904
904
|
* a single try/catch so that a bad issue costs its own run and nothing else.
|
|
905
905
|
*/
|
|
906
|
+
/**
|
|
907
|
+
* Whether a turns-cap kill is handed straight back to the queue.
|
|
908
|
+
*
|
|
909
|
+
* A turns cap consumes the independent continuation budget rather than an
|
|
910
|
+
* implementation-failure attempt, so a run that ran out of turns mid-work is
|
|
911
|
+
* requeued and the next tick reattaches its branch (#50).
|
|
912
|
+
*
|
|
913
|
+
* The artifact test is what keeps that from becoming a loop. A run that reached
|
|
914
|
+
* the ceiling with no PR, no observed head and no salvage commit produced
|
|
915
|
+
* nothing to continue *from*, and requeueing it spends another continuation on
|
|
916
|
+
* the same spin — the seven rows on this fleet that burned $109 producing no
|
|
917
|
+
* merged PR. That shape is `turn-cap-spinning`, which #132 says must escalate
|
|
918
|
+
* with evidence instead; requeueing it here would settle the question before the
|
|
919
|
+
* classifier ever saw the row, and the queue label would already be back on.
|
|
920
|
+
*
|
|
921
|
+
* Pure so the rule is testable without a worker session — the same split as
|
|
922
|
+
* {@link settlementFor}.
|
|
923
|
+
*/
|
|
924
|
+
export function shouldContinueAfterTurnsCap(f: {
|
|
925
|
+
killedBy?: string;
|
|
926
|
+
prUrl?: string;
|
|
927
|
+
headSha?: string;
|
|
928
|
+
salvageSha?: string;
|
|
929
|
+
continuation: number;
|
|
930
|
+
maxContinuations: number;
|
|
931
|
+
}): boolean {
|
|
932
|
+
if (f.killedBy !== "turns") return false;
|
|
933
|
+
const hasArtifacts = f.prUrl !== undefined || f.headSha !== undefined || f.salvageSha !== undefined;
|
|
934
|
+
if (!hasArtifacts) return false;
|
|
935
|
+
return hasContinuationBudget(f.continuation, f.maxContinuations);
|
|
936
|
+
}
|
|
937
|
+
|
|
906
938
|
async function handleIssue(d: Deps, r: Routed, attempt: number): Promise<void> {
|
|
907
939
|
const { project, caps, tracker, store } = d;
|
|
908
940
|
const issue = r.issue.number;
|
|
@@ -1299,13 +1331,17 @@ async function handleIssue(d: Deps, r: Routed, attempt: number): Promise<void> {
|
|
|
1299
1331
|
detail: [`${r.issue.title}`, r.issue.url, "", ...salvaged, "", result.report].join("\n"),
|
|
1300
1332
|
});
|
|
1301
1333
|
} else if (state === "failed" || state === "killed") {
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
1304
|
-
// count includes the segment that just ended.
|
|
1334
|
+
// The row is already `killed`, so this count includes the segment that
|
|
1335
|
+
// just ended.
|
|
1305
1336
|
const continuation = store.continuationsFor(project.name, issue);
|
|
1306
|
-
const continueTurns =
|
|
1307
|
-
result.killedBy
|
|
1308
|
-
|
|
1337
|
+
const continueTurns = shouldContinueAfterTurnsCap({
|
|
1338
|
+
killedBy: result.killedBy,
|
|
1339
|
+
prUrl: result.prUrl,
|
|
1340
|
+
headSha: result.headSha,
|
|
1341
|
+
salvageSha: settlement?.patch?.salvageSha,
|
|
1342
|
+
continuation,
|
|
1343
|
+
maxContinuations: caps.maxContinuationsPerIssue,
|
|
1344
|
+
});
|
|
1309
1345
|
|
|
1310
1346
|
if (continueTurns) {
|
|
1311
1347
|
await tracker.removeLabel(issue, inProgress);
|
|
@@ -2891,8 +2927,13 @@ export async function classifyAndRecover(d: Deps): Promise<void> {
|
|
|
2891
2927
|
// base does move under it.
|
|
2892
2928
|
if (run.state === "pushed-green" && cls === "unknown") continue;
|
|
2893
2929
|
|
|
2930
|
+
const retry = run.failureClass !== undefined;
|
|
2894
2931
|
store.updateRun(run.id, { failureClass: cls, recoveryAction: recovery });
|
|
2895
|
-
log(
|
|
2932
|
+
log(
|
|
2933
|
+
retry
|
|
2934
|
+
? `#${run.issue} retrying ${recovery} for ${cls}: ${evidence}`
|
|
2935
|
+
: `#${run.issue} classified ${cls} → ${recovery}: ${evidence}`,
|
|
2936
|
+
);
|
|
2896
2937
|
await recoverRun(d, run, cls, recovery, evidence);
|
|
2897
2938
|
}
|
|
2898
2939
|
}
|
package/src/store.ts
CHANGED
|
@@ -735,10 +735,20 @@ export function openStore(dbPath: string): Store {
|
|
|
735
735
|
// Newest first, and bounded: every row this returns costs `gh` calls to gather
|
|
736
736
|
// facts for, so a fleet with a long unclassified history classifies over
|
|
737
737
|
// several ticks rather than spending one tick's budget on all of it.
|
|
738
|
+
//
|
|
739
|
+
// Classified-but-unrecovered rows are included, not just unclassified ones: a
|
|
740
|
+
// recovery whose tracker write failed has to come back, or the log line saying
|
|
741
|
+
// "retrying next tick" is a lie and the row is stranded with a class and no
|
|
742
|
+
// action. `hold` is excluded because it is *recorded only* by design — its
|
|
743
|
+
// `recoveredAt` stays NULL forever, and re-offering it would spin the sweep.
|
|
738
744
|
const selectUnclassified = db.query<RunRow, [string, number]>(
|
|
739
745
|
`SELECT * FROM runs
|
|
740
|
-
WHERE project = ?
|
|
746
|
+
WHERE project = ?
|
|
741
747
|
AND state IN ('blocked', 'killed', 'failed', 'orphaned', 'pushed-green')
|
|
748
|
+
AND (
|
|
749
|
+
failureClass IS NULL
|
|
750
|
+
OR (recoveredAt IS NULL AND recoveryAction IN ('settle', 'continue', 'requeue', 'rerun-checks'))
|
|
751
|
+
)
|
|
742
752
|
ORDER BY startedAt DESC, rowid DESC
|
|
743
753
|
LIMIT ?`,
|
|
744
754
|
);
|
package/src/tracker/github.ts
CHANGED
|
@@ -113,18 +113,29 @@ interface GhPrVerification {
|
|
|
113
113
|
|
|
114
114
|
/** Carries the captured stderr so callers can classify a failure without
|
|
115
115
|
* re-running the command or parsing the message text of a plain Error. */
|
|
116
|
-
class GhError extends Error {
|
|
116
|
+
export class GhError extends Error {
|
|
117
117
|
readonly argv: string[];
|
|
118
118
|
readonly code: number;
|
|
119
119
|
readonly stderr: string;
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Whatever the command printed before it failed.
|
|
122
|
+
*
|
|
123
|
+
* Carried because a nonzero exit is not always a failure to *read*: `gh pr
|
|
124
|
+
* checks` exits 1 when a check failed and 8 when one is pending, having
|
|
125
|
+
* already written the complete JSON payload asked for. Discarding stdout here
|
|
126
|
+
* made both CI failure classes unreachable on exactly the pull requests they
|
|
127
|
+
* exist to classify — a red PR answered "no checks" (#132 follow-up).
|
|
128
|
+
*/
|
|
129
|
+
readonly stdout: string;
|
|
130
|
+
|
|
131
|
+
constructor(argv: string[], code: number, stderr: string, stdout = "") {
|
|
122
132
|
const detail = stderr.trim() || "(no stderr)";
|
|
123
133
|
super(`\`gh ${argv.join(" ")}\` exited ${code}: ${detail}`);
|
|
124
134
|
this.name = "GhError";
|
|
125
135
|
this.argv = argv;
|
|
126
136
|
this.code = code;
|
|
127
137
|
this.stderr = stderr;
|
|
138
|
+
this.stdout = stdout;
|
|
128
139
|
}
|
|
129
140
|
}
|
|
130
141
|
|
|
@@ -155,7 +166,7 @@ async function gh(argv: string[], stdin?: string): Promise<string> {
|
|
|
155
166
|
|
|
156
167
|
const signal = proc.signalCode;
|
|
157
168
|
if (code !== 0 || signal) {
|
|
158
|
-
throw new GhError(argv, code, signal ? `${stderr}\nterminated by ${signal}` : stderr);
|
|
169
|
+
throw new GhError(argv, code, signal ? `${stderr}\nterminated by ${signal}` : stderr, stdout);
|
|
159
170
|
}
|
|
160
171
|
return stdout;
|
|
161
172
|
}
|
|
@@ -671,11 +682,17 @@ export function makeTracker(p: ProjectConfig, runGh: typeof gh = gh): Tracker {
|
|
|
671
682
|
if (!PR_URL.test(prUrl)) return [];
|
|
672
683
|
try {
|
|
673
684
|
return checkConclusionsFrom(await runGh(["pr", "checks", prUrl, "--json", "name,state,link"]));
|
|
674
|
-
} catch {
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
|
|
685
|
+
} catch (err) {
|
|
686
|
+
// A nonzero exit is the *documented* answer for a PR with a failing or
|
|
687
|
+
// pending check — `gh pr checks` exits 1 and 8 respectively, having
|
|
688
|
+
// already printed the whole payload. Reading only the throw is what made
|
|
689
|
+
// `ci-infra` and `ci-deterministic` unreachable on live data: every red
|
|
690
|
+
// PR answered "no checks", so it classified `unknown` and escalated.
|
|
691
|
+
//
|
|
692
|
+
// So the payload wins when there is one, and only a genuinely unreadable
|
|
693
|
+
// answer falls through to "could not tell".
|
|
694
|
+
const stdout = err instanceof GhError ? err.stdout : "";
|
|
695
|
+
return checkConclusionsFrom(stdout);
|
|
679
696
|
}
|
|
680
697
|
},
|
|
681
698
|
|
package/src/types.ts
CHANGED
|
@@ -1299,10 +1299,14 @@ export interface Store {
|
|
|
1299
1299
|
/** Everything an operator still has to care about: pending, sending, failed. */
|
|
1300
1300
|
openReports(project: string): ReportRecord[];
|
|
1301
1301
|
/**
|
|
1302
|
-
* Terminal rows the classification sweep
|
|
1303
|
-
* and bounded (#132)
|
|
1304
|
-
*
|
|
1305
|
-
*
|
|
1302
|
+
* Terminal rows the classification sweep still owes something to, newest first
|
|
1303
|
+
* and bounded (#132): never classified, or classified with a retryable recovery
|
|
1304
|
+
* that has not run. The second half is what makes "retrying next tick" true
|
|
1305
|
+
* when a tracker write fails mid-recovery.
|
|
1306
|
+
*
|
|
1307
|
+
* Bounded because each row costs tracker calls to gather facts for: a fleet
|
|
1308
|
+
* with a long unclassified history works through it over several ticks rather
|
|
1309
|
+
* than spending one tick on all of it.
|
|
1306
1310
|
*/
|
|
1307
1311
|
runsNeedingClassification(project: string, limit?: number): RunRecord[];
|
|
1308
1312
|
/** Unrecovered rows per class, for `status`. Empty when nothing is carrying one. */
|