omp-conductor 0.19.3 → 0.19.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 +4 -3
- package/package.json +1 -1
- package/src/daemon.ts +91 -1
- package/src/fleet.ts +11 -5
- package/src/settlement.ts +24 -10
- package/src/store.ts +1 -1
package/README.md
CHANGED
|
@@ -386,9 +386,10 @@ message on stderr) on every uncertainty:
|
|
|
386
386
|
- the tick config does not parse — the agent name would be a guess
|
|
387
387
|
- `herdr agent list` is unreachable, prints nothing, or prints output with no
|
|
388
388
|
explicit `agents` array; only a real `agents: []` means "no agents"
|
|
389
|
-
- an agent row is unreadable — a missing `
|
|
390
|
-
present with a non-string value. An
|
|
391
|
-
|
|
389
|
+
- an agent row is unreadable — a missing `pane_id`, a non-string non-null
|
|
390
|
+
`name`, or an `agent` field present with a non-string value. An absent or
|
|
391
|
+
`null` `name` is an unnamed pane; an absent or `null` `agent` is the sticky
|
|
392
|
+
claim herdr reports after the agent exits, and both stay normal answers
|
|
392
393
|
- the configured agent name is not unique, or the claimed pane runs some other agent
|
|
393
394
|
- `pane process-info` fails, or the claim is live `omp` but names no recognizable
|
|
394
395
|
omp foreground PID — "cannot see it" is never reported as "it is stopped"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.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
|
@@ -116,11 +116,13 @@ import {
|
|
|
116
116
|
import {
|
|
117
117
|
adoptSalvagedPrs,
|
|
118
118
|
classifyAndRecover,
|
|
119
|
+
harnessLogDir,
|
|
119
120
|
collectSettlementFlags,
|
|
120
121
|
formatQuarantinedRuns,
|
|
121
122
|
formatSalvagedRuns,
|
|
122
123
|
reactToProviderCredit,
|
|
123
124
|
readSessionError,
|
|
125
|
+
readTerminalEvidence,
|
|
124
126
|
reconcileOrphanedRuns,
|
|
125
127
|
reconcileGroomingClosures,
|
|
126
128
|
reconcileStaleLabels,
|
|
@@ -133,6 +135,7 @@ import { materializeOmpSettings } from "./omp-settings.ts";
|
|
|
133
135
|
import { evaluateDecisionConditions, probeNpmVersion, probeRateLimitReset } from "./decisions.ts";
|
|
134
136
|
import {
|
|
135
137
|
infraLogSignature,
|
|
138
|
+
classifyRun,
|
|
136
139
|
infraSignatureVersion,
|
|
137
140
|
providerCreditRefusal,
|
|
138
141
|
providerTransientFault,
|
|
@@ -333,6 +336,8 @@ interface Deps {
|
|
|
333
336
|
workerControls: WorkerControlRegistry;
|
|
334
337
|
/** Session seam for lifecycle integration tests; production uses the real harness. */
|
|
335
338
|
workerDeps?: RunWorkerDeps;
|
|
339
|
+
/** Harness per-process logs used to classify terminal worker routes. */
|
|
340
|
+
harnessLogDir?: string;
|
|
336
341
|
integrity: IntegrityGate;
|
|
337
342
|
stall: StallGate;
|
|
338
343
|
/**
|
|
@@ -3959,7 +3964,10 @@ export async function handleReviewRevision(d: Deps, revision: ReviewRevisionReco
|
|
|
3959
3964
|
if (await settleStopBeforeSession()) return;
|
|
3960
3965
|
if (await settleDrainBeforeSession()) return;
|
|
3961
3966
|
|
|
3962
|
-
|
|
3967
|
+
// A review revision is a new worker process on the same run row. Clear the
|
|
3968
|
+
// prior process evidence before launch so a failed wake cannot inherit an
|
|
3969
|
+
// earlier round's harness route.
|
|
3970
|
+
store.updateRun(runId, { worktree: worktreePath, workerPid: null, terminalEvidence: null });
|
|
3963
3971
|
|
|
3964
3972
|
// The row's own counters are cumulative across the revision: the revision
|
|
3965
3973
|
// worker meters its own session from zero, so its deltas are added to the
|
|
@@ -3972,6 +3980,7 @@ export async function handleReviewRevision(d: Deps, revision: ReviewRevisionReco
|
|
|
3972
3980
|
log(`#${issue} review round ${revision.round} → resuming ${branch} (${priorSessionFile})`);
|
|
3973
3981
|
|
|
3974
3982
|
|
|
3983
|
+
let workerStartedAt: number | undefined;
|
|
3975
3984
|
let result: WorkerResult;
|
|
3976
3985
|
try {
|
|
3977
3986
|
const runAllowanceUsd = runSpendAllowanceUsd(caps);
|
|
@@ -3997,7 +4006,9 @@ export async function handleReviewRevision(d: Deps, revision: ReviewRevisionReco
|
|
|
3997
4006
|
socketPath: join(sessionDir, "ipc.sock"),
|
|
3998
4007
|
verbSocketPath: verbListener.path,
|
|
3999
4008
|
onSpawn: (pid) => {
|
|
4009
|
+
workerStartedAt = Date.now();
|
|
4000
4010
|
verbListener?.bindPid(pid);
|
|
4011
|
+
store.updateRun(runId, { workerPid: pid });
|
|
4001
4012
|
},
|
|
4002
4013
|
onChildLog: (line) => {
|
|
4003
4014
|
log(`#${issue} ${line}`);
|
|
@@ -4092,6 +4103,18 @@ export async function handleReviewRevision(d: Deps, revision: ReviewRevisionReco
|
|
|
4092
4103
|
...(verified.reason === undefined ? [] : ["", verified.reason]),
|
|
4093
4104
|
result.report,
|
|
4094
4105
|
].join("\n");
|
|
4106
|
+
const latestRun = store.getRun(runId) ?? run;
|
|
4107
|
+
const terminalEvidence =
|
|
4108
|
+
state === "failed" || state === "killed"
|
|
4109
|
+
? readTerminalEvidence(
|
|
4110
|
+
{
|
|
4111
|
+
sessionFile: result.sessionFile,
|
|
4112
|
+
startedAt: workerStartedAt ?? latestRun.startedAt,
|
|
4113
|
+
...(latestRun.workerPid === undefined ? {} : { workerPid: latestRun.workerPid }),
|
|
4114
|
+
},
|
|
4115
|
+
d.harnessLogDir ?? harnessLogDir(),
|
|
4116
|
+
)
|
|
4117
|
+
: undefined;
|
|
4095
4118
|
|
|
4096
4119
|
const terminalPatch: Partial<RunRecord> = {
|
|
4097
4120
|
endedAt: Date.now(),
|
|
@@ -4112,9 +4135,76 @@ export async function handleReviewRevision(d: Deps, revision: ReviewRevisionReco
|
|
|
4112
4135
|
...(result.headSha === undefined ? {} : { headSha: result.headSha }),
|
|
4113
4136
|
sessionFile: result.sessionFile,
|
|
4114
4137
|
...(result.graphTools === undefined ? {} : { graphTools: result.graphTools }),
|
|
4138
|
+
...(terminalEvidence === undefined ? {} : { terminalEvidence }),
|
|
4115
4139
|
report: finalReport,
|
|
4116
4140
|
...settlement?.patch,
|
|
4117
4141
|
};
|
|
4142
|
+
const terminalClassification =
|
|
4143
|
+
terminalEvidence === undefined
|
|
4144
|
+
? undefined
|
|
4145
|
+
: classifyRun(
|
|
4146
|
+
{
|
|
4147
|
+
...latestRun,
|
|
4148
|
+
...terminalPatch,
|
|
4149
|
+
state,
|
|
4150
|
+
terminalEvidence,
|
|
4151
|
+
},
|
|
4152
|
+
{},
|
|
4153
|
+
caps,
|
|
4154
|
+
);
|
|
4155
|
+
if (terminalClassification?.cls === "model-empty-stop") {
|
|
4156
|
+
const recoveredAt = Date.now();
|
|
4157
|
+
store.updateRun(runId, {
|
|
4158
|
+
...terminalPatch,
|
|
4159
|
+
state: "failed",
|
|
4160
|
+
failureClass: terminalClassification.cls,
|
|
4161
|
+
recoveryAction: terminalClassification.recovery,
|
|
4162
|
+
recoveredAt,
|
|
4163
|
+
});
|
|
4164
|
+
const continuations = store.continuationsFor(project.name, issue);
|
|
4165
|
+
if (hasContinuationBudget(continuations, caps.maxContinuationsPerIssue)) {
|
|
4166
|
+
// Keep the same review row, round, findings, session, branch and PR.
|
|
4167
|
+
// Restore the exact pushed-green origin state while moving this stop
|
|
4168
|
+
// into the explicit continuation counter; the next claim must not
|
|
4169
|
+
// depend on re-verifying a failed-state origin at an unchanged head.
|
|
4170
|
+
const chargedRun = store.getRun(runId) ?? latestRun;
|
|
4171
|
+
store.updateRun(runId, {
|
|
4172
|
+
...terminalPatch,
|
|
4173
|
+
state: "pushed-green",
|
|
4174
|
+
failureClass: null,
|
|
4175
|
+
recoveryAction: null,
|
|
4176
|
+
recoveredAt: null,
|
|
4177
|
+
continuationCharges: (chargedRun.continuationCharges ?? 0) + 1,
|
|
4178
|
+
workerPid: null,
|
|
4179
|
+
lastError: terminalClassification.evidence,
|
|
4180
|
+
});
|
|
4181
|
+
store.requeueReviewRevision(revision.id);
|
|
4182
|
+
log(
|
|
4183
|
+
`#${issue} review round ${revision.round} re-queued after a provider empty-stop ` +
|
|
4184
|
+
`(${continuations}/${caps.maxContinuationsPerIssue} continuations): ${terminalClassification.evidence}`,
|
|
4185
|
+
);
|
|
4186
|
+
return;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
store.settleReviewRevision(revision.id, "failed", recoveredAt);
|
|
4190
|
+
swapLabel(store, project.name, issue, inProgress, project.stateLabels.failed);
|
|
4191
|
+
const model = result.model ?? latestRun.resolvedModel ?? latestRun.model ?? "configured model";
|
|
4192
|
+
const provider = result.provider ?? latestRun.resolvedProvider ?? "provider";
|
|
4193
|
+
await safeEscalate(d, {
|
|
4194
|
+
tier: 1,
|
|
4195
|
+
project: project.name,
|
|
4196
|
+
issue,
|
|
4197
|
+
runId,
|
|
4198
|
+
summary: `#${issue} review round ${revision.round}: the model kept returning empty responses`,
|
|
4199
|
+
detail:
|
|
4200
|
+
`${provider}/${model} ended ${continuations} review session(s) with empty responses until ` +
|
|
4201
|
+
`the harness retry cap. The same review round was preserved while continuation budget remained; ` +
|
|
4202
|
+
`that budget is now exhausted at ${caps.maxContinuationsPerIssue}. No implementation failure or ` +
|
|
4203
|
+
`additional review round was charged.\n\n${terminalClassification.evidence}`,
|
|
4204
|
+
});
|
|
4205
|
+
log(`#${issue} review round ${revision.round} provider empty-stops exhausted the continuation budget`);
|
|
4206
|
+
return;
|
|
4207
|
+
}
|
|
4118
4208
|
// #903: an infrastructure kill spends no review round.
|
|
4119
4209
|
//
|
|
4120
4210
|
// A dispatch that dies before the resumed session takes a turn — a host
|
package/src/fleet.ts
CHANGED
|
@@ -815,7 +815,7 @@ export function startHerdrFleet(projectName?: string, deps: HerdrStartDeps = {})
|
|
|
815
815
|
}
|
|
816
816
|
|
|
817
817
|
export interface HerdrAgent {
|
|
818
|
-
name
|
|
818
|
+
name?: string;
|
|
819
819
|
paneId: string;
|
|
820
820
|
agent?: string;
|
|
821
821
|
sessionPath?: string;
|
|
@@ -1568,16 +1568,22 @@ export function parseHerdrAgentList(rawOutput: string): HerdrAgent[] {
|
|
|
1568
1568
|
const agent = row as { readonly [key: string]: unknown };
|
|
1569
1569
|
const name = agent["name"];
|
|
1570
1570
|
const paneId = agent["pane_id"];
|
|
1571
|
-
if (typeof
|
|
1571
|
+
if (typeof paneId !== "string") {
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
"herdr agent list row is missing a string `pane_id` — cannot identify the pane",
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
if (name !== undefined && name !== null && typeof name !== "string") {
|
|
1572
1577
|
throw new Error(
|
|
1573
|
-
|
|
1578
|
+
`herdr agent list row for pane ${paneId} has a non-string \`name\` (${typeof name}) — ` +
|
|
1574
1579
|
"cannot tell whether it is the conductor pane",
|
|
1575
1580
|
);
|
|
1576
1581
|
}
|
|
1582
|
+
const parsedName = typeof name === "string" ? name : undefined;
|
|
1577
1583
|
const rawAgent = agent["agent"];
|
|
1578
1584
|
if (rawAgent !== undefined && rawAgent !== null && typeof rawAgent !== "string") {
|
|
1579
1585
|
throw new Error(
|
|
1580
|
-
`herdr agent list row for ${
|
|
1586
|
+
`herdr agent list row for ${parsedName ?? `pane ${paneId}`} has a non-string \`agent\` (${typeof rawAgent}) — ` +
|
|
1581
1587
|
`cannot tell whether an agent is live`,
|
|
1582
1588
|
);
|
|
1583
1589
|
}
|
|
@@ -1591,7 +1597,7 @@ export function parseHerdrAgentList(rawOutput: string): HerdrAgent[] {
|
|
|
1591
1597
|
}
|
|
1592
1598
|
}
|
|
1593
1599
|
out.push({
|
|
1594
|
-
name,
|
|
1600
|
+
...(parsedName === undefined ? {} : { name: parsedName }),
|
|
1595
1601
|
paneId,
|
|
1596
1602
|
...(liveAgent === undefined ? {} : { agent: liveAgent }),
|
|
1597
1603
|
...(sessionPath === undefined ? {} : { sessionPath }),
|
package/src/settlement.ts
CHANGED
|
@@ -1287,10 +1287,10 @@ export function harnessLogDir(): string {
|
|
|
1287
1287
|
* per process. These name why a session ended when the row cannot: the run
|
|
1288
1288
|
* itself records no error, because from its side nothing failed (#986). */
|
|
1289
1289
|
const HARNESS_ROUTE_MARKERS = [
|
|
1290
|
-
"empty-stop-retry-cap",
|
|
1291
|
-
"empty-stop-handled",
|
|
1292
|
-
"bottom-checkCompaction",
|
|
1293
|
-
"context-overflow",
|
|
1290
|
+
{ marker: "empty-stop-retry-cap", terminal: true },
|
|
1291
|
+
{ marker: "empty-stop-handled", terminal: false },
|
|
1292
|
+
{ marker: "bottom-checkCompaction", terminal: false },
|
|
1293
|
+
{ marker: "context-overflow", terminal: true },
|
|
1294
1294
|
] as const;
|
|
1295
1295
|
|
|
1296
1296
|
/**
|
|
@@ -1373,13 +1373,27 @@ function readHarnessRoute(
|
|
|
1373
1373
|
} catch {
|
|
1374
1374
|
return undefined;
|
|
1375
1375
|
}
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1376
|
+
// A terminal failure route remains the cause even when the harness performs
|
|
1377
|
+
// post-agent maintenance afterwards. The live #1001 logs record
|
|
1378
|
+
// `empty-stop-retry-cap` and then `bottom-checkCompaction` milliseconds later;
|
|
1379
|
+
// choosing the last marker hid the failure behind routine bookkeeping.
|
|
1378
1380
|
let found: string | undefined;
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
if (
|
|
1382
|
-
|
|
1381
|
+
let foundAt = -1;
|
|
1382
|
+
for (const route of HARNESS_ROUTE_MARKERS) {
|
|
1383
|
+
if (!route.terminal) continue;
|
|
1384
|
+
const at = text.lastIndexOf(route.marker);
|
|
1385
|
+
if (at > foundAt) {
|
|
1386
|
+
found = route.marker;
|
|
1387
|
+
foundAt = at;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (found !== undefined) return `harness route ${found}`;
|
|
1391
|
+
for (const route of HARNESS_ROUTE_MARKERS) {
|
|
1392
|
+
const at = text.lastIndexOf(route.marker);
|
|
1393
|
+
if (at > foundAt) {
|
|
1394
|
+
found = route.marker;
|
|
1395
|
+
foundAt = at;
|
|
1396
|
+
}
|
|
1383
1397
|
}
|
|
1384
1398
|
return found === undefined ? undefined : `harness route ${found}`;
|
|
1385
1399
|
}
|
package/src/store.ts
CHANGED
|
@@ -3758,7 +3758,7 @@ export function openStore(dbPath: string): Store {
|
|
|
3758
3758
|
ELSE 0
|
|
3759
3759
|
END,
|
|
3760
3760
|
continuationCharges = COALESCE(continuationCharges, 0) + CASE
|
|
3761
|
-
WHEN state = 'failed' AND failureClass
|
|
3761
|
+
WHEN state = 'failed' AND failureClass IN ('returned-for-revision', 'model-empty-stop') THEN 1
|
|
3762
3762
|
WHEN state = 'killed' AND (failureClass IS NULL OR failureClass NOT IN (${CHARGEABLE_CONTINUATION_EXCLUSIONS})) THEN 1
|
|
3763
3763
|
ELSE 0
|
|
3764
3764
|
END
|