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 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 `name`/`pane_id`, or an `agent` field
390
- present with a non-string value. An *absent* or `null` `agent` is the sticky
391
- claim herdr reports after the agent exits, and stays a normal answer
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",
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
- store.updateRun(runId, { worktree: worktreePath });
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: string;
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 name !== "string" || typeof paneId !== "string") {
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
- "herdr agent list row is missing a string `name`/`pane_id`" +
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 ${name} has a non-string \`agent\` (${typeof rawAgent}) — ` +
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
- // Last match wins: the terminal route is the one that ended the session, and
1377
- // `empty-stop-handled` appearing earlier is a retry that worked.
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
- for (const marker of HARNESS_ROUTE_MARKERS) {
1380
- const at = text.lastIndexOf(marker);
1381
- if (at === -1) continue;
1382
- if (found === undefined || at > text.lastIndexOf(found)) found = marker;
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 = 'returned-for-revision' THEN 1
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