omp-conductor 0.7.0 → 0.7.1
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/package.json +1 -1
- package/src/failure-class.ts +36 -19
- package/src/store.ts +43 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
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/failure-class.ts
CHANGED
|
@@ -149,6 +149,35 @@ export function neverStarted(
|
|
|
149
149
|
return undefined;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Evidence that the conductor's own git path failed before the worker's first
|
|
154
|
+
* turn, or `undefined` when this row is something else.
|
|
155
|
+
*
|
|
156
|
+
* A mirror ref-lock (#168), a dead mirror, a transiently unreachable host: the
|
|
157
|
+
* worker never touched the issue, so none of this should charge an
|
|
158
|
+
* implementation attempt. `lastError` of this shape is written only by the
|
|
159
|
+
* dispatch catch (`errText(err)` = the stack, which prefixes the thrown
|
|
160
|
+
* "git … exited N: …" message with "Error: ") and by state messages — never by
|
|
161
|
+
* a worker — so it is proof of the daemon's own git rather than a worker's
|
|
162
|
+
* failed push.
|
|
163
|
+
*
|
|
164
|
+
* Exported for the same reason {@link neverStarted} is: the store's one-time
|
|
165
|
+
* repair has to recognise exactly what the classifier does, or history and
|
|
166
|
+
* future disagree about which rows were the daemon's fault.
|
|
167
|
+
*/
|
|
168
|
+
export function dispatchInfra(
|
|
169
|
+
run: Pick<RunRecord, "turns" | "lastError" | "prUrl" | "headSha" | "salvageSha">,
|
|
170
|
+
): string | undefined {
|
|
171
|
+
if (run.turns !== 0) return undefined;
|
|
172
|
+
if (run.prUrl !== undefined || run.headSha !== undefined || run.salvageSha !== undefined) return undefined;
|
|
173
|
+
if (run.lastError === undefined || !DISPATCH_GIT_ERROR.test(run.lastError)) return undefined;
|
|
174
|
+
return `dispatch failed in the conductor's own git path before turn 1: ${run.lastError.split("\n")[0]}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// See {@link dispatchInfra}: the "Error: " prefix is what `errText` puts in
|
|
178
|
+
// front of the thrown message, so a bare /^git / would never match a real row.
|
|
179
|
+
const DISPATCH_GIT_ERROR = /^(?:Error: )?git .+ exited \d+/s;
|
|
180
|
+
|
|
152
181
|
export function classifyRun(run: RunRecord, facts: ClassifyFacts): Classification {
|
|
153
182
|
const hasArtifacts = run.prUrl !== undefined || run.headSha !== undefined || run.salvageSha !== undefined;
|
|
154
183
|
|
|
@@ -184,25 +213,13 @@ export function classifyRun(run: RunRecord, facts: ClassifyFacts): Classificatio
|
|
|
184
213
|
}
|
|
185
214
|
|
|
186
215
|
// Dispatch infra: the conductor's own git path failed before the worker's
|
|
187
|
-
// first turn
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
run.state === "failed" &&
|
|
196
|
-
run.turns === 0 &&
|
|
197
|
-
!hasArtifacts &&
|
|
198
|
-
run.lastError !== undefined &&
|
|
199
|
-
/^(?:Error: )?git .+ exited \d+/s.test(run.lastError)
|
|
200
|
-
) {
|
|
201
|
-
return {
|
|
202
|
-
cls: "dispatch-infra",
|
|
203
|
-
recovery: "requeue",
|
|
204
|
-
evidence: `dispatch failed in the conductor's own git path before turn 1: ${run.lastError.split("\n")[0]}`,
|
|
205
|
-
};
|
|
216
|
+
// first turn. See {@link dispatchInfra} — the worker never touched the issue,
|
|
217
|
+
// so this must not charge an implementation attempt.
|
|
218
|
+
if (run.state === "failed") {
|
|
219
|
+
const evidence = dispatchInfra(run);
|
|
220
|
+
if (evidence !== undefined) {
|
|
221
|
+
return { cls: "dispatch-infra", recovery: "requeue", evidence };
|
|
222
|
+
}
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
if (run.state === "blocked") {
|
package/src/store.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { dirname, join } from "node:path";
|
|
|
15
15
|
|
|
16
16
|
import { stateDir } from "./config.ts";
|
|
17
17
|
import { DECISION_TTL_MS, DEFAULT_CAPS } from "./types.ts";
|
|
18
|
-
import { neverStarted } from "./failure-class.ts";
|
|
18
|
+
import { dispatchInfra, neverStarted } from "./failure-class.ts";
|
|
19
19
|
import type {
|
|
20
20
|
DecisionDraft,
|
|
21
21
|
FailureClass,
|
|
@@ -726,6 +726,48 @@ export function openStore(dbPath: string): Store {
|
|
|
726
726
|
for (const row of misread) reclassify.run(row.id);
|
|
727
727
|
}
|
|
728
728
|
|
|
729
|
+
// The same repair, for the case the one above deliberately leaves alone: its
|
|
730
|
+
// comment names it exactly ("a turn-0 row carrying an unrecognised error (a
|
|
731
|
+
// failed mirror fetch, say) is untouched"). #177 gave that fault a name, but
|
|
732
|
+
// only for rows written after it existed. Every earlier one was logged
|
|
733
|
+
// `unknown`, whose recovery is `escalate` — so `runsNeedingClassification`
|
|
734
|
+
// never revisits it and `countFailures` counts it forever. With
|
|
735
|
+
// `maxAttemptsPerIssue` defaulting to 2, a single mirror ref-lock is half an
|
|
736
|
+
// issue's entire budget spent on a fault that was never in its code.
|
|
737
|
+
//
|
|
738
|
+
// Narrow the same way: exactly `unknown`, state `failed`, and the
|
|
739
|
+
// CLASSIFIER's own predicate rather than a second copy of the rule.
|
|
740
|
+
// Deliberately only `failureClass` — not `recoveryAction`/`recoveredAt`.
|
|
741
|
+
// Relieving the budget is the point; re-animating a months-old run into the
|
|
742
|
+
// requeue sweep is not, and clearing the recovery columns would do exactly
|
|
743
|
+
// that. Idempotent for free: these rows then read `dispatch-infra`, which the
|
|
744
|
+
// WHERE no longer matches.
|
|
745
|
+
const mirrorBroke = db
|
|
746
|
+
.query<
|
|
747
|
+
{ id: string; lastError: string | null; prUrl: string | null; headSha: string | null; salvageSha: string | null },
|
|
748
|
+
[]
|
|
749
|
+
>(
|
|
750
|
+
`SELECT id, lastError, prUrl, headSha, salvageSha FROM runs
|
|
751
|
+
WHERE failureClass = 'unknown' AND turns = 0 AND state = 'failed'`,
|
|
752
|
+
)
|
|
753
|
+
.all()
|
|
754
|
+
.filter(
|
|
755
|
+
(row) =>
|
|
756
|
+
dispatchInfra({
|
|
757
|
+
turns: 0,
|
|
758
|
+
...(row.lastError === null ? {} : { lastError: row.lastError }),
|
|
759
|
+
...(row.prUrl === null ? {} : { prUrl: row.prUrl }),
|
|
760
|
+
...(row.headSha === null ? {} : { headSha: row.headSha }),
|
|
761
|
+
...(row.salvageSha === null ? {} : { salvageSha: row.salvageSha }),
|
|
762
|
+
}) !== undefined,
|
|
763
|
+
);
|
|
764
|
+
if (mirrorBroke.length > 0) {
|
|
765
|
+
const reclassify = db.query<unknown, [string]>(
|
|
766
|
+
`UPDATE runs SET failureClass = 'dispatch-infra' WHERE id = ?`,
|
|
767
|
+
);
|
|
768
|
+
for (const row of mirrorBroke) reclassify.run(row.id);
|
|
769
|
+
}
|
|
770
|
+
|
|
729
771
|
const insertRun = db.query<unknown, SqlValue[]>(
|
|
730
772
|
`INSERT INTO runs (
|
|
731
773
|
id, project, issue, repo, branch, worktree, state, attempt, turns,
|