omp-conductor 0.19.2 → 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.
@@ -170,6 +170,16 @@ const START_FAILURE_SIGNATURES = [
170
170
  // symptom this replaced (the child dying on the peer import) already read as
171
171
  // a start failure, so charging one here would be a regression dressed as a fix.
172
172
  "worker identity unavailable",
173
+ // The 2026-08-2x host faults (#986), each recorded verbatim in `lastError`
174
+ // by the launcher or the worker identity gate — all before the session took
175
+ // a turn:
176
+ // - the child died before it could connect to the dispatcher;
177
+ // - node could not resolve a package out of the worker harness tree;
178
+ // - the worker's settings file was unreadable (the EACCES outage).
179
+ "exited 1 before connecting",
180
+ "cannot find package",
181
+ "enoent while resolving package",
182
+ "failed to read settings config",
173
183
  ] as const;
174
184
 
175
185
  /**
@@ -395,6 +405,128 @@ export function adminRestartAttribution(lastError: string | undefined): string |
395
405
  return firstLine.trim();
396
406
  }
397
407
 
408
+ /** The launcher's own statement that the session child died mid-run, before
409
+ * the session ended. Written by `omp.ts` when the child exits while the
410
+ * dispatcher still owns it; the exit code is the child's own, so a signal
411
+ * death reads >=128 (143 = SIGTERM, 137 = SIGKILL) — an external kill,
412
+ * never anything the worker's work decided (#986). */
413
+ const CHILD_EXIT_MARKER = /session child exited (\d+) before the session ended/;
414
+
415
+ /**
416
+ * Evidence that this run's session child was terminated by a signal, or
417
+ * `undefined` when it was not.
418
+ *
419
+ * Read from the surfaces a run persists — `lastError` first, then `report`,
420
+ * because the prompt-throw path records the launcher's message as the report
421
+ * and leaves `lastError` empty. Deliberately narrowed to signal-range codes:
422
+ * a clean nonzero exit is some process's verdict nobody has named yet, and
423
+ * naming it `admin-kill` would waive an attempt that may have been spent.
424
+ */
425
+ export function childSignalDeath(
426
+ run: Pick<RunRecord, "lastError" | "report">,
427
+ ): string | undefined {
428
+ const text = run.lastError ?? run.report;
429
+ if (text === undefined) return undefined;
430
+ const match = CHILD_EXIT_MARKER.exec(text);
431
+ if (match === null) return undefined;
432
+ const code = Number(match[1]);
433
+ if (!Number.isFinite(code) || code < 128) return undefined;
434
+ return match[0];
435
+ }
436
+
437
+ /** The reason the settle sweep writes beside a reviewer-closed PR
438
+ * (`settlementFor`), matched as recorded evidence so rows written before
439
+ * that sweep stamped a class read the same way (#986). */
440
+ const REVIEWER_CLOSED_MARKER = "closed without merging";
441
+
442
+ /**
443
+ * Evidence that a reviewer closed this run's pushed work without merging it,
444
+ * or `undefined` when nothing says so. Two independent signals, either
445
+ * sufficient: the tracker fact (`facts.pr === "closed"`), or the settle
446
+ * sweep's own recorded reason in `lastError`. A review decision is not a
447
+ * worker failure, which is exactly what `returned-for-revision` means; the
448
+ * only new reading here is applying it to rows whose PR closed while the row
449
+ * itself had already gone terminal-failed.
450
+ */
451
+ export function reviewerClosed(
452
+ run: Pick<RunRecord, "lastError" | "prUrl">,
453
+ facts: ClassifyFacts,
454
+ ): string | undefined {
455
+ if (run.lastError !== undefined && run.lastError.includes(REVIEWER_CLOSED_MARKER)) {
456
+ return run.lastError.split("\n")[0]?.trim();
457
+ }
458
+ if (facts.pr === "closed" && run.prUrl !== undefined) {
459
+ return `${run.prUrl} closed without merging`;
460
+ }
461
+ return undefined;
462
+ }
463
+
464
+ /** The harness's own name for giving up on a provider that kept answering with
465
+ * empty assistant turns: it retries, then ends the session on the retry cap
466
+ * and says so in its maintenance routing (`route: "empty-stop-retry-cap"`,
467
+ * beside `Assistant returned empty stop after retry cap; try switching
468
+ * models`). A closed marker written by the harness, not vendor prose. */
469
+ const EMPTY_STOP_MARKER = "empty-stop-retry-cap";
470
+
471
+ /**
472
+ * Evidence that the provider answered with empty turns until the harness's own
473
+ * retry cap ended the session, or `undefined` when nothing recorded says so
474
+ * (#986).
475
+ *
476
+ * Read from the terminal evidence the settlement sweep recorded, because this
477
+ * marker is written by the harness's session log rather than by the transcript
478
+ * or the row — the run itself has no error, no verdict and no idea it died,
479
+ * which is precisely why two of these read as `unknown` on 2026-08-23 at
480
+ * 100/180 and 54/180 turns while a third run on the same model finished green
481
+ * in the same window.
482
+ *
483
+ * Provider flake, not worker logic: the recovery continues from whatever the
484
+ * attempt pushed rather than charging an implementation attempt for it.
485
+ */
486
+ export function modelEmptyStop(
487
+ run: Pick<RunRecord, "terminalEvidence">,
488
+ ): string | undefined {
489
+ if (run.terminalEvidence === undefined) return undefined;
490
+ if (!run.terminalEvidence.includes(EMPTY_STOP_MARKER)) return undefined;
491
+ return run.terminalEvidence;
492
+ }
493
+
494
+ /**
495
+ * Evidence that this run ended without ever delivering a settlement verdict,
496
+ * or `undefined` when something on the row says otherwise (#986).
497
+ *
498
+ * The shape: state `failed`, turns taken, no error anywhere, and a final
499
+ * report whose words are mid-delivery narration — "Now I'll execute …", "Type
500
+ * check passes. Now the test suite:" — with no `state:`/`status:` line the
501
+ * result parser could read. The session reached its worker and died between
502
+ * turns without handing over; measured over two weeks of the reference fleet,
503
+ * this one shape was half of all `unknown`.
504
+ *
505
+ * Deliberately narrow at every edge: turn zero belongs to
506
+ * {@link neverStarted}, any error text keeps the row `unknown` (an
507
+ * unrecognised error says something happened), and an absent report leaves
508
+ * genuinely nothing to name. The class charges its attempt exactly as
509
+ * `unknown` did — only the name becomes specific.
510
+ */
511
+ export function noVerdictExit(run: RunRecord): string | undefined {
512
+ if (run.state !== "failed") return undefined;
513
+ if (run.turns <= 0) return undefined;
514
+ // Blank counts as absent: a row whose `lastError` is an empty string carries
515
+ // no error information, and reading it as "something happened" is what kept
516
+ // four real rows unnamed. Any non-blank text, recognised or not, still wins —
517
+ // an unrecognised error says something happened.
518
+ if (run.lastError !== undefined && run.lastError.trim() !== "") return undefined;
519
+ if (run.report === undefined || run.report.trim() === "") return undefined;
520
+ // Mirror the two verdict spellings the result path reads (`worker.ts`
521
+ // STATE_LINE_PATTERN for prose settlements, `renderSettlement`'s leading
522
+ // `status:` line for structured yields). Either one present means a verdict
523
+ // was delivered and this function must stay silent.
524
+ if (/^state:\s*\S+\s*$/im.test(run.report)) return undefined;
525
+ if (/^status:\s*\S+\s*$/im.test(run.report)) return undefined;
526
+ const lastWords = run.report.split("\n")[0]?.trim() ?? "";
527
+ return `the session ended without delivering a settlement verdict; last words: "${lastWords.slice(0, 80)}"`;
528
+ }
529
+
398
530
  export function classifyRun(
399
531
  run: RunRecord,
400
532
  facts: ClassifyFacts,
@@ -424,6 +556,23 @@ export function classifyRun(
424
556
  };
425
557
  }
426
558
 
559
+ // The launcher's own statement that the session child died on a signal
560
+ // mid-run: an external kill — a drain, a restart, the host — not anything
561
+ // the worker's work decided (#986). Ahead of the provider branches on
562
+ // purpose: a transcript can carry an older, long-recovered stream fault,
563
+ // and the launcher's terminal statement outranks whatever the session said
564
+ // earlier. Requeued free like every other administrative kill.
565
+ if (run.state === "failed") {
566
+ const marker = childSignalDeath(run);
567
+ if (marker !== undefined) {
568
+ return {
569
+ cls: "admin-kill",
570
+ recovery: "requeue",
571
+ evidence: `killed externally (${marker}) — the session child was terminated, not finished`,
572
+ };
573
+ }
574
+ }
575
+
427
576
  // A billing state, not an implementation failure. Its own class because it is
428
577
  // the one outage an operator fixes with a card rather than a diagnosis, and
429
578
  // because burying a self-describing provider error in `unknown` erodes what
@@ -507,6 +656,19 @@ export function classifyRun(
507
656
  }
508
657
  }
509
658
 
659
+ // A reviewer closed this run's pushed work without merging it: a review
660
+ // decision, not a worker failure (#986). Two rows reach here — the settle
661
+ // sweep's own recorded reason in `lastError` on a row that predates the
662
+ // class, and a live `closed` tracker fact on a row whose worker died after
663
+ // pushing but before claiming. Recovery `none` mirrors the settle sweep
664
+ // exactly: the queue label is the remedy, and no action is performed here.
665
+ if (run.state === "failed") {
666
+ const closed = reviewerClosed(run, facts);
667
+ if (closed !== undefined) {
668
+ return { cls: "returned-for-revision", recovery: "none", evidence: closed };
669
+ }
670
+ }
671
+
510
672
  if (run.state === "blocked") {
511
673
  return {
512
674
  cls: "question",
@@ -639,6 +801,36 @@ export function classifyRun(
639
801
  }
640
802
  }
641
803
 
804
+ // The provider answered with empty turns until the harness gave up (#986).
805
+ // Below every branch that reads a real error on purpose: a row carrying its
806
+ // own provider fault is that fault, and this marker only decides rows that
807
+ // would otherwise be `unknown`. Continues rather than requeues, because the
808
+ // attempt's pushed work is the honest place to resume from, and a provider
809
+ // flake must not charge an implementation attempt (#1001). `killed` rows
810
+ // never reach here — a cap kill is the operative fact about a run that hit
811
+ // its ceiling, whatever the provider did earlier in the session.
812
+ if (run.state === "failed") {
813
+ const emptyStop = modelEmptyStop(run);
814
+ if (emptyStop !== undefined) {
815
+ return {
816
+ cls: "model-empty-stop",
817
+ recovery: "continue",
818
+ evidence: `the provider returned empty turns until the harness's retry cap ended the session (${emptyStop}) — turns ${run.turns}/${run.maxTurns}`,
819
+ };
820
+ }
821
+ }
822
+
823
+ // The session reached its worker and died between turns without handing over
824
+ // a verdict (#986): the largest single shape inside `unknown`. Named rather
825
+ // than requeued blind — an operator reading `status` learns the run stopped
826
+ // mid-delivery, which is a different question from "nothing is known".
827
+ if (run.state === "failed") {
828
+ const narration = noVerdictExit(run);
829
+ if (narration !== undefined) {
830
+ return { cls: "no-verdict", recovery: "escalate", evidence: narration };
831
+ }
832
+ }
833
+
642
834
  // Deliberately escalate rather than retry. An unrecognised shape is a gap in
643
835
  // this table, and a silent requeue would spend a budget on a cause nobody has
644
836
  // named — the exact behaviour #132 exists to end.