@ai-dossier/sched 0.23.2 → 0.24.0
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 +97 -14
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +228 -14
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/dispatch-health.d.ts +84 -0
- package/dist/dispatch-health.d.ts.map +1 -0
- package/dist/dispatch-health.js +127 -0
- package/dist/dispatch-health.js.map +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +285 -71
- package/dist/engine.js.map +1 -1
- package/dist/enqueue.d.ts.map +1 -1
- package/dist/enqueue.js +1 -0
- package/dist/enqueue.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +11 -1
- package/dist/scheduler.js.map +1 -1
- package/dist/state.d.ts +49 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +166 -12
- package/dist/state.js.map +1 -1
- package/dist/status.d.ts +9 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +2 -0
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +153 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +27 -3
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -73,6 +73,7 @@ exports.runLoop = runLoop;
|
|
|
73
73
|
const core_1 = require("@ai-dossier/core");
|
|
74
74
|
const batch_dispatch_1 = require("./batch-dispatch");
|
|
75
75
|
const dispatch_1 = require("./dispatch");
|
|
76
|
+
const dispatch_health_1 = require("./dispatch-health");
|
|
76
77
|
const groundtruth_1 = require("./groundtruth");
|
|
77
78
|
const journal_1 = require("./journal");
|
|
78
79
|
const labels_1 = require("./labels");
|
|
@@ -405,7 +406,9 @@ function reconcileLabelBlocks(ctx, state, poll) {
|
|
|
405
406
|
const ownStillPresent = labels.some((name) => name.toLowerCase() === blockedBy.toLowerCase());
|
|
406
407
|
const current = (0, labels_1.pickHardBlockLabel)(labels);
|
|
407
408
|
if (!ownStillPresent && current === null) {
|
|
408
|
-
|
|
409
|
+
// #633: an unblocked entry is a fresh attempt too — see
|
|
410
|
+
// `requeueOrphanedDispatches`.
|
|
411
|
+
next = (0, state_1.transitionIssue)(next, issue, 'queued', { reason: null, ...state_1.CLEARED_ENTRY_DEDUP_MARKERS }, now);
|
|
409
412
|
journal(ctx, 'label-cleared', unit, { reason: (0, labels_1.labelBlockReason)(blockedBy) });
|
|
410
413
|
ctx.result.labelCleared.push(unit);
|
|
411
414
|
}
|
|
@@ -723,7 +726,14 @@ function writeFence(ctx, unit, issue, slot, truth) {
|
|
|
723
726
|
* `unit-failed` line otherwise — and that indistinguishability is exactly
|
|
724
727
|
* what cost imboard-monorepo#3999 (docs/agent-traps.md).
|
|
725
728
|
*/
|
|
726
|
-
function failOrAdoptOpenPr(ctx, state, unit, slot, report,
|
|
729
|
+
function failOrAdoptOpenPr(ctx, state, unit, slot, report,
|
|
730
|
+
// #629: never `'dispatch-failure'` in practice — `enterRecovery` only
|
|
731
|
+
// reaches this function under `escalate: true` (its cap-check is itself
|
|
732
|
+
// gated on `escalate`), and a `dispatch-failure` cause always passes
|
|
733
|
+
// `escalate: false`. Kept as the two-member union `enterRecovery` uses
|
|
734
|
+
// before its escalate check, so a maintainer reading this signature does
|
|
735
|
+
// not read "a spend wall can terminally fail a unit" as a live path.
|
|
736
|
+
causeEvent, cause, evidence) {
|
|
727
737
|
let prCheck = 'skipped';
|
|
728
738
|
if (!report && causeEvent === 'verify-incomplete') {
|
|
729
739
|
if (slot.branch === null) {
|
|
@@ -746,6 +756,13 @@ function failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, ev
|
|
|
746
756
|
});
|
|
747
757
|
}
|
|
748
758
|
prCheck = openPr === undefined ? 'unreachable' : 'none';
|
|
759
|
+
// #632: confirmed NOT one of the per-tick re-emit sites — this
|
|
760
|
+
// function always ends by either parking the unit (the branch above)
|
|
761
|
+
// or falling through to the unconditional `failUnit` below, so the
|
|
762
|
+
// unit is terminal (or parked) by the time this call returns. There is
|
|
763
|
+
// no "next tick" on which the SAME dispatch reaches this check again,
|
|
764
|
+
// so no dedup marker is needed here; the line fires exactly once, on
|
|
765
|
+
// the one terminal decision it accompanies.
|
|
749
766
|
if (prCheck === 'unreachable') {
|
|
750
767
|
journal(ctx, 'ground-truth-unreachable', unit, {
|
|
751
768
|
slot: slot.id,
|
|
@@ -767,7 +784,7 @@ function failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, ev
|
|
|
767
784
|
// with `causeEvent === 'verify-incomplete'` thread it through
|
|
768
785
|
// `completeUnitOrRecover`: `reconcileRunning`'s dead-pid rail, from
|
|
769
786
|
// `recordDispatchRunLog`'s own read, and (since #620) `reconcileSlots`'
|
|
770
|
-
// `exited`/`verifying` rail, from `
|
|
787
|
+
// `exited`/`verifying` rail, from `readDispatchSignalsForSlot`. The two agree —
|
|
771
788
|
// the dispatch log is static once the agent has exited — so the tool name
|
|
772
789
|
// survives regardless of which tick reaches this decision. It stays
|
|
773
790
|
// OPTIONAL: a slice with no parseable `tool_use` yields null, hence the
|
|
@@ -789,7 +806,14 @@ function failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, ev
|
|
|
789
806
|
* mechanical-starting ladder and fail MERGED-AWARE at the cap: the PR is
|
|
790
807
|
* already merged, so dependents stay released.
|
|
791
808
|
*/
|
|
792
|
-
function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
|
|
809
|
+
function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {},
|
|
810
|
+
// #629: `escalate: false` for a CONFIRMED provider API error — it must
|
|
811
|
+
// never consume the per-unit escalation ladder or the ESCALATION_CAP
|
|
812
|
+
// (a spend/rate wall is not the issue's fault), so the respawn keeps the
|
|
813
|
+
// slot's CURRENT tier and `recoveries` count unchanged. Every other caller
|
|
814
|
+
// keeps the default (`true`), unaffected.
|
|
815
|
+
options = {}) {
|
|
816
|
+
const escalate = options.escalate ?? true;
|
|
793
817
|
const issue = (0, journal_1.issueOfUnit)(unit);
|
|
794
818
|
if (issue === null)
|
|
795
819
|
return state;
|
|
@@ -800,21 +824,36 @@ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
|
|
|
800
824
|
return state;
|
|
801
825
|
killUnitAgent(ctx, state, unit);
|
|
802
826
|
// #524: only the STALL path kills a still-live, not-yet-recorded agent —
|
|
803
|
-
// `causeEvent === 'verify-incomplete'`
|
|
804
|
-
// AFTER the agent's exit was already detected and
|
|
805
|
-
// branch of `reconcileRunning`; recording again
|
|
806
|
-
// that same dispatch. #591: a stall kill still has
|
|
807
|
-
// reading for `last_tool` — a hung agent's last
|
|
808
|
-
// an operator what it hung in.
|
|
827
|
+
// `causeEvent === 'verify-incomplete'`/`'dispatch-failure'` arrive from
|
|
828
|
+
// `completeUnitOrRecover` AFTER the agent's exit was already detected and
|
|
829
|
+
// recorded by the dead-pid branch of `reconcileRunning`; recording again
|
|
830
|
+
// here would double-count that same dispatch. #591: a stall kill still has
|
|
831
|
+
// a fresh log slice worth reading for `last_tool` — a hung agent's last
|
|
832
|
+
// tool is exactly what tells an operator what it hung in.
|
|
809
833
|
if (causeEvent === 'stalled') {
|
|
810
|
-
const stallLastTool = recordDispatchRunLog(ctx, state, slot, unit);
|
|
834
|
+
const stallLastTool = recordDispatchRunLog(ctx, state, slot, unit).lastTool;
|
|
811
835
|
if (stallLastTool !== null)
|
|
812
836
|
evidence = { ...evidence, last_tool: stallLastTool };
|
|
813
837
|
}
|
|
814
838
|
const report = (0, state_1.isReportSlot)(slot);
|
|
815
|
-
|
|
816
|
-
if (
|
|
817
|
-
|
|
839
|
+
let resolvedTier;
|
|
840
|
+
if (escalate) {
|
|
841
|
+
const escalated = report ? (0, dispatch_1.reportTierFor)(slot.recoveries + 1) : (0, dispatch_1.escalateTier)(entry.tier);
|
|
842
|
+
if (slot.recoveries >= types_1.ESCALATION_CAP || escalated === null) {
|
|
843
|
+
// #629: unreachable with `causeEvent === 'dispatch-failure'` — that
|
|
844
|
+
// cause always passes `escalate: false` above, so this branch (and the
|
|
845
|
+
// narrower `causeEvent` type `failOrAdoptOpenPr` declares) is never
|
|
846
|
+
// actually asked to terminally fail a unit over a provider wall.
|
|
847
|
+
return failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, evidence);
|
|
848
|
+
}
|
|
849
|
+
resolvedTier = escalated;
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
// An unescalated redispatch keeps the CURRENT tier — `reportTierFor`
|
|
853
|
+
// evaluated at the unchanged `slot.recoveries` for a report slot (mirrors
|
|
854
|
+
// `recordDispatchRunLog`'s own tier expression), `entry.tier` verbatim
|
|
855
|
+
// otherwise — never `escalateTier`/`recoveries + 1`.
|
|
856
|
+
resolvedTier = report ? ((0, dispatch_1.reportTierFor)(slot.recoveries) ?? entry.tier) : entry.tier;
|
|
818
857
|
}
|
|
819
858
|
// Fence BEFORE the respawn (#504 AC1/AC4): `killUnitAgent` above only reaches a pid
|
|
820
859
|
// this process can see and signal, and #472 proved that is not the same as a dead
|
|
@@ -823,17 +862,17 @@ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
|
|
|
823
862
|
const fenced = writeFence(ctx, unit, issue, slot, truth);
|
|
824
863
|
let next = (0, state_1.transitionSlot)(state, slot.id, 'recovering', {
|
|
825
864
|
pid: null,
|
|
826
|
-
recoveries: slot.recoveries + 1,
|
|
865
|
+
recoveries: escalate ? slot.recoveries + 1 : slot.recoveries,
|
|
827
866
|
gen: fenced ?? slot.gen,
|
|
828
867
|
// Only a fence that actually landed starts the short takeover watch: an
|
|
829
868
|
// unfenced redispatch is already degraded, and cutting its allowance down
|
|
830
869
|
// would compound one failure with another.
|
|
831
870
|
fenced_at: fenced === null ? null : now.toISOString(),
|
|
832
871
|
}, now);
|
|
833
|
-
if (!report) {
|
|
872
|
+
if (!report && escalate) {
|
|
834
873
|
next = {
|
|
835
874
|
...next,
|
|
836
|
-
entries: next.entries.map((e) => e.issue === issue ? { ...e, tier:
|
|
875
|
+
entries: next.entries.map((e) => e.issue === issue ? { ...e, tier: resolvedTier, updated_at: now.toISOString() } : e),
|
|
837
876
|
};
|
|
838
877
|
}
|
|
839
878
|
journal(ctx, causeEvent, unit, {
|
|
@@ -842,10 +881,22 @@ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
|
|
|
842
881
|
...(slot.last_progress_at !== null ? { last_progress_at: slot.last_progress_at } : {}),
|
|
843
882
|
...evidence,
|
|
844
883
|
});
|
|
884
|
+
// #629: an UNESCALATED redispatch (a confirmed provider API error) has no
|
|
885
|
+
// natural bound — `recoveries`/`ESCALATION_CAP` never advance for it — so
|
|
886
|
+
// once the dispatch-health pause has fired, respawning it every tick would
|
|
887
|
+
// reproduce the exact incident this fix exists to stop, just on the
|
|
888
|
+
// per-issue rail instead of the batch one (`runBatchTick` gets the
|
|
889
|
+
// equivalent gate below). Hold the slot in `recovering`, unspawned, until
|
|
890
|
+
// `sched resume`: `reconcileRecovering` (below) is what retries it once the
|
|
891
|
+
// pause clears — no separate re-classification happens while parked, since
|
|
892
|
+
// this function is reached exactly once per dead dispatch.
|
|
893
|
+
if (!escalate && next.paused) {
|
|
894
|
+
return next;
|
|
895
|
+
}
|
|
845
896
|
journal(ctx, 'redispatched', unit, {
|
|
846
|
-
tier:
|
|
897
|
+
tier: resolvedTier,
|
|
847
898
|
slot: slot.id,
|
|
848
|
-
...(0, dispatch_1.journalCmdModelFields)((0, dispatch_1.resolveTierSpawn)(ctx.dispatch,
|
|
899
|
+
...(0, dispatch_1.journalCmdModelFields)((0, dispatch_1.resolveTierSpawn)(ctx.dispatch, resolvedTier, issue)),
|
|
849
900
|
});
|
|
850
901
|
ctx.result.redispatched.push(unit);
|
|
851
902
|
// Respawn immediately on the recovering rail — recovering → running. A
|
|
@@ -980,6 +1031,96 @@ function applyProgressSignals(ctx, state, slot, truth, unit) {
|
|
|
980
1031
|
function effectiveClosedSignal(slot, truth) {
|
|
981
1032
|
return (0, state_1.isReportSlot)(slot) ? false : truth.closed;
|
|
982
1033
|
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Journal `event` for `unit` on the first tick of a new streak, then again
|
|
1036
|
+
* only every `JOURNAL_DEDUP_REANNOUNCE_TICKS` ticks while it persists (#632).
|
|
1037
|
+
* That window is a TICK count, not a duration: the sites reached every
|
|
1038
|
+
* reconcile re-announce at ~20 min on the default 60 s interval, while
|
|
1039
|
+
* `reconcileParked`/`reconcileStaleFailedParks` are gated on `prPoll.ran` and
|
|
1040
|
+
* so advance once per 150 s PR poll, ~50 min. Both intervals are
|
|
1041
|
+
* operator-tunable — which is why each entry also carries `since`
|
|
1042
|
+
* — `ground-truth-unreachable` and `pr-watch-waiting` were previously
|
|
1043
|
+
* journaled every tick the condition held, for as long as it lasted (an
|
|
1044
|
+
* outage produced one entry every reconcile interval, per affected unit, for
|
|
1045
|
+
* up to `max_slots` units at once).
|
|
1046
|
+
*
|
|
1047
|
+
* `sinceOf`/`ticksOf` read the entry's marker for THIS event family and
|
|
1048
|
+
* `withMarker` writes it back — `ground-truth-unreachable` and
|
|
1049
|
+
* `pr-watch-waiting` dedup independently via two separate marker pairs on
|
|
1050
|
+
* `QueueEntry` (mirrors #630's `pr_watch_failed_*` on `BatchEntry`, scoped
|
|
1051
|
+
* to the issue instead of the batch: these sites span both slot-held units
|
|
1052
|
+
* and parked/stale-failed ones with no live slot, and `QueueEntry` is the
|
|
1053
|
+
* one record every unit has either way).
|
|
1054
|
+
*
|
|
1055
|
+
* A caller with no `QueueEntry` to key on (should not happen for a real
|
|
1056
|
+
* issue-dispatch unit) journals unconditionally rather than silently drop
|
|
1057
|
+
* the line — the pre-#632 behavior, and safe: it can only make the journal
|
|
1058
|
+
* more verbose, never hide a real condition.
|
|
1059
|
+
*
|
|
1060
|
+
* Returns the patched state — callers must thread it, or the marker is lost
|
|
1061
|
+
* and the event re-fires next tick as if nothing had been recorded.
|
|
1062
|
+
*/
|
|
1063
|
+
function journalConditionIfDue(ctx, state, unit, event, sinceOf, ticksOf, withMarker, extra) {
|
|
1064
|
+
const issue = (0, journal_1.issueOfUnit)(unit);
|
|
1065
|
+
const entry = issue === null ? undefined : (0, state_1.findEntry)(state, issue);
|
|
1066
|
+
if (issue === null || entry === undefined) {
|
|
1067
|
+
journal(ctx, event, unit, extra);
|
|
1068
|
+
return state;
|
|
1069
|
+
}
|
|
1070
|
+
const isNewStreak = sinceOf(entry) === null;
|
|
1071
|
+
const ticks = isNewStreak ? 1 : ticksOf(entry) + 1;
|
|
1072
|
+
const now = ctx.deps.now();
|
|
1073
|
+
const since = isNewStreak ? now.toISOString() : sinceOf(entry);
|
|
1074
|
+
if (isNewStreak || ticks % types_1.JOURNAL_DEDUP_REANNOUNCE_TICKS === 0) {
|
|
1075
|
+
// `at` is the decision clock (AC3); `since` is the streak's onset. Both
|
|
1076
|
+
// are needed: `ticks_persisted` is a TICK count, and the two rails tick at
|
|
1077
|
+
// different, operator-tunable rates, so it maps to no fixed wall-clock.
|
|
1078
|
+
journal(ctx, event, unit, {
|
|
1079
|
+
...extra,
|
|
1080
|
+
at: now.toISOString(),
|
|
1081
|
+
since,
|
|
1082
|
+
ticks_persisted: ticks,
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
// `touchUpdatedAt: false` — this marker is dedup bookkeeping, not a
|
|
1086
|
+
// substantive change to the entry; `QueueEntry.updated_at` is relied on
|
|
1087
|
+
// elsewhere (`isStaleFailedPark`'s window, `status.ts`'s "since" display,
|
|
1088
|
+
// `readiness.ts`'s dispatch tiebreak) as a clock that must not reset on a
|
|
1089
|
+
// silent tick.
|
|
1090
|
+
return (0, state_1.patchEntry)(state, issue, withMarker(since, ticks), now, false);
|
|
1091
|
+
}
|
|
1092
|
+
function journalGroundTruthUnreachableIfDue(ctx, state, unit, extra) {
|
|
1093
|
+
return journalConditionIfDue(ctx, state, unit, 'ground-truth-unreachable', (e) => e.ground_truth_unreachable_since, (e) => e.ground_truth_unreachable_ticks, (since, ticks) => ({
|
|
1094
|
+
ground_truth_unreachable_since: since,
|
|
1095
|
+
ground_truth_unreachable_ticks: ticks,
|
|
1096
|
+
}), extra);
|
|
1097
|
+
}
|
|
1098
|
+
function journalPrWatchWaitingIfDue(ctx, state, unit, extra) {
|
|
1099
|
+
return journalConditionIfDue(ctx, state, unit, 'pr-watch-waiting', (e) => e.pr_watch_waiting_since, (e) => e.pr_watch_waiting_ticks, (since, ticks) => ({ pr_watch_waiting_since: since, pr_watch_waiting_ticks: ticks }), extra);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Clear a `QueueEntry`'s `ground-truth-unreachable` marker once truth
|
|
1103
|
+
* answers again (#632) — a no-op when nothing was set, so callers can call
|
|
1104
|
+
* this unconditionally on every tick truth is healthy without churning
|
|
1105
|
+
* `updated_at` for entries that were never in a streak.
|
|
1106
|
+
*/
|
|
1107
|
+
function clearGroundTruthUnreachable(state, unit, now) {
|
|
1108
|
+
const issue = (0, journal_1.issueOfUnit)(unit);
|
|
1109
|
+
const entry = issue === null ? undefined : (0, state_1.findEntry)(state, issue);
|
|
1110
|
+
if (issue === null || entry === undefined || entry.ground_truth_unreachable_since === null) {
|
|
1111
|
+
return state;
|
|
1112
|
+
}
|
|
1113
|
+
return (0, state_1.patchEntry)(state, issue, { ground_truth_unreachable_since: null, ground_truth_unreachable_ticks: 0 }, now, false);
|
|
1114
|
+
}
|
|
1115
|
+
/** Same as `clearGroundTruthUnreachable`, for the `pr-watch-waiting` marker. */
|
|
1116
|
+
function clearPrWatchWaiting(state, unit, now) {
|
|
1117
|
+
const issue = (0, journal_1.issueOfUnit)(unit);
|
|
1118
|
+
const entry = issue === null ? undefined : (0, state_1.findEntry)(state, issue);
|
|
1119
|
+
if (issue === null || entry === undefined || entry.pr_watch_waiting_since === null) {
|
|
1120
|
+
return state;
|
|
1121
|
+
}
|
|
1122
|
+
return (0, state_1.patchEntry)(state, issue, { pr_watch_waiting_since: null, pr_watch_waiting_ticks: 0 }, now, false);
|
|
1123
|
+
}
|
|
983
1124
|
/**
|
|
984
1125
|
* #575: journal the ignored stale milestone — shared by `reconcileRunning`'s
|
|
985
1126
|
* external-advance check and `completeUnitOrRecover`'s verify-complete check,
|
|
@@ -1026,22 +1167,24 @@ function journalStaleMilestoneIfIgnored(ctx, state, unit, slot, truth, verifiedC
|
|
|
1026
1167
|
});
|
|
1027
1168
|
return (0, state_1.patchSlot)(state, slot.id, { stale_milestone_ignored_for: slot.spawned_at }, now);
|
|
1028
1169
|
}
|
|
1170
|
+
const NO_DISPATCH_SIGNALS = { lastTool: null, apiError: null };
|
|
1029
1171
|
/**
|
|
1030
|
-
* Read THIS dispatch's last tool call
|
|
1031
|
-
* pure, side-effect-free parse, safe to
|
|
1032
|
-
*
|
|
1033
|
-
* `
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
* same answer every time.
|
|
1172
|
+
* Read THIS dispatch's last tool call and API-error classification (#591,
|
|
1173
|
+
* #620, #629) from its log slice — a pure, side-effect-free parse, safe to
|
|
1174
|
+
* call from any slot status and any number of times: unlike
|
|
1175
|
+
* `recordDispatchRunLog` below, it writes nothing to `runs.jsonl`, so it
|
|
1176
|
+
* carries no exactly-once constraint. Exists because a `verify-incomplete`/
|
|
1177
|
+
* `dispatch-failure` decision can land on a LATER tick than the one that
|
|
1178
|
+
* detected the dead pid (e.g. ground truth was unreachable in between) — by
|
|
1179
|
+
* then the slot has moved past `running` and `recordDispatchRunLog`'s guard
|
|
1180
|
+
* refuses to re-read, but the dispatch's log file is static once the agent
|
|
1181
|
+
* has exited, so re-parsing it here yields the same answer every time.
|
|
1040
1182
|
*/
|
|
1041
|
-
function
|
|
1183
|
+
function readDispatchSignalsForSlot(ctx, slot, unit) {
|
|
1042
1184
|
if (slot.spawned_at === null)
|
|
1043
|
-
return
|
|
1044
|
-
|
|
1185
|
+
return NO_DISPATCH_SIGNALS;
|
|
1186
|
+
const { content } = dispatchLogSlice(ctx, slot, unit);
|
|
1187
|
+
return { lastTool: (0, core_1.parseLastToolUse)(content), apiError: (0, core_1.parseDispatchApiError)(content) };
|
|
1045
1188
|
}
|
|
1046
1189
|
/**
|
|
1047
1190
|
* THIS dispatch's slice of the per-unit log: path, start offset, contents.
|
|
@@ -1088,7 +1231,7 @@ function recordDispatchRunLog(ctx, state, slot, unit) {
|
|
|
1088
1231
|
reason: slot.spawned_at === null ? 'never-spawned' : `already-recorded-${slot.status}`,
|
|
1089
1232
|
slot: slot.id,
|
|
1090
1233
|
});
|
|
1091
|
-
return
|
|
1234
|
+
return NO_DISPATCH_SIGNALS;
|
|
1092
1235
|
}
|
|
1093
1236
|
const issue = (0, journal_1.issueOfUnit)(unit);
|
|
1094
1237
|
const entry = issue === null ? undefined : (0, state_1.findEntry)(state, issue);
|
|
@@ -1100,7 +1243,7 @@ function recordDispatchRunLog(ctx, state, slot, unit) {
|
|
|
1100
1243
|
journal(ctx, 'run-log-skipped', unit, {
|
|
1101
1244
|
reason: issue === null ? 'not-an-issue-unit' : 'entry-gone',
|
|
1102
1245
|
});
|
|
1103
|
-
return
|
|
1246
|
+
return NO_DISPATCH_SIGNALS;
|
|
1104
1247
|
}
|
|
1105
1248
|
// Report slots ride the same tier the cycle escalation ladder set at
|
|
1106
1249
|
// dispatch time, EXCEPT `entry.tier` is deliberately not updated for a
|
|
@@ -1129,7 +1272,9 @@ function recordDispatchRunLog(ctx, state, slot, unit) {
|
|
|
1129
1272
|
(0, run_log_1.finalizeRunLogEntry)(runEntry, logContent, ctx.deps.homeDir, (event, extra) => journal(ctx, event, unit, extra), { log: logFile, offset });
|
|
1130
1273
|
// #591: the last tool this dispatch called, so an unverified exit attributes to a
|
|
1131
1274
|
// concrete cause (e.g. `Monitor`) without opening the transcript — see `enterRecovery`.
|
|
1132
|
-
|
|
1275
|
+
// #629: whether this SAME dispatch was a confirmed provider API error — see
|
|
1276
|
+
// `completeUnitOrRecover`.
|
|
1277
|
+
return { lastTool: (0, core_1.parseLastToolUse)(logContent), apiError: (0, core_1.parseDispatchApiError)(logContent) };
|
|
1133
1278
|
}
|
|
1134
1279
|
/** Reconcile one running slot against its polled ground truth. */
|
|
1135
1280
|
function reconcileRunning(ctx, state, slot, truth, unit) {
|
|
@@ -1138,21 +1283,25 @@ function reconcileRunning(ctx, state, slot, truth, unit) {
|
|
|
1138
1283
|
// is DETECTED, never trusted as completion (AC2/AC3).
|
|
1139
1284
|
if (slot.pid !== null && !ctx.deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
|
|
1140
1285
|
journal(ctx, 'exit-detected', unit, { pid: slot.pid, slot: slot.id });
|
|
1141
|
-
const
|
|
1286
|
+
const signals = recordDispatchRunLog(ctx, state, slot, unit);
|
|
1142
1287
|
const exited = (0, state_1.transitionSlot)(state, slot.id, 'exited', {}, now);
|
|
1143
|
-
return completeUnitOrRecover(ctx, exited, unit, truth, 'verify-complete',
|
|
1288
|
+
return completeUnitOrRecover(ctx, exited, unit, truth, 'verify-complete', signals);
|
|
1144
1289
|
}
|
|
1145
1290
|
// The milestone poll FAILED (gh outage, missing binary) — unreachable is NOT
|
|
1146
1291
|
// known-absent (decision 2, option A): stall and advance decisions pause for
|
|
1147
1292
|
// this unit until truth returns. The dead-pid rail above still ran — local
|
|
1148
1293
|
// truth needs no network.
|
|
1149
1294
|
if (!truth.reachable) {
|
|
1150
|
-
|
|
1295
|
+
return journalGroundTruthUnreachableIfDue(ctx, state, unit, {
|
|
1151
1296
|
slot: slot.id,
|
|
1152
1297
|
detail: 'stall/advance decisions paused until truth returns',
|
|
1153
1298
|
});
|
|
1154
|
-
return state;
|
|
1155
1299
|
}
|
|
1300
|
+
// #632: truth answered this tick — any unreachable streak recorded against
|
|
1301
|
+
// a PREVIOUS tick is over.
|
|
1302
|
+
// Named for what it holds — a SchedState with the streak cleared — not for
|
|
1303
|
+
// `truth.reachable`, the boolean five lines up.
|
|
1304
|
+
const cleared = clearGroundTruthUnreachable(state, unit, now);
|
|
1156
1305
|
// Ground truth says the unit is DONE while the agent still holds the slot —
|
|
1157
1306
|
// externally-advanced state (AC3): reclaim the slot, kill the leftover agent.
|
|
1158
1307
|
// A parked milestone is deliberately NOT an advance: a detached run parks
|
|
@@ -1165,7 +1314,7 @@ function reconcileRunning(ctx, state, slot, truth, unit) {
|
|
|
1165
1314
|
const verifiedComplete = (0, groundtruth_1.isVerifiedComplete)(truth.milestone, closedSignal, slot.spawned_at);
|
|
1166
1315
|
// Threaded, not discarded: the once-per-dispatch marker lives in the state
|
|
1167
1316
|
// this returns.
|
|
1168
|
-
const marked = journalStaleMilestoneIfIgnored(ctx,
|
|
1317
|
+
const marked = journalStaleMilestoneIfIgnored(ctx, cleared, unit, slot, truth, verifiedComplete, closedSignal);
|
|
1169
1318
|
if (verifiedComplete && !(0, groundtruth_1.isParkedMilestone)(truth.milestone)) {
|
|
1170
1319
|
journal(ctx, 'external-advance', unit, {
|
|
1171
1320
|
pid: slot.pid,
|
|
@@ -1262,11 +1411,13 @@ function recordDispatchOutcome(ctx, state, unit, slot, suspect) {
|
|
|
1262
1411
|
* event.
|
|
1263
1412
|
*/
|
|
1264
1413
|
function completeUnitOrRecover(ctx, state, unit, truth, via,
|
|
1265
|
-
// #591: the last tool
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
// `
|
|
1414
|
+
// #591/#629: the last tool called AND whether the exit was a confirmed
|
|
1415
|
+
// provider API error (from `recordDispatchRunLog`'s dead-pid reading),
|
|
1416
|
+
// threaded through to the unverified-exit decision below. The
|
|
1417
|
+
// `external-advance` caller passes none — it DOES record a run log
|
|
1418
|
+
// (`reconcileRunning`, #524), but ground truth already confirmed
|
|
1419
|
+
// completion, so this path never reaches that decision and neither signal
|
|
1420
|
+
// has anything to attribute.
|
|
1270
1421
|
//
|
|
1271
1422
|
// #620: a THUNK, not a value, for the deferred `exited`/`verifying` rail — that caller
|
|
1272
1423
|
// has no read of its own to hand over and would otherwise have to read the dispatch log
|
|
@@ -1275,7 +1426,7 @@ function completeUnitOrRecover(ctx, state, unit, truth, via,
|
|
|
1275
1426
|
// MAX_DISPATCH_LOG_BYTES (32 MiB) and its size is set by the spawned agent, so an
|
|
1276
1427
|
// eager read is up to 32 MiB per slot per tick for as long as an outage holds slots in
|
|
1277
1428
|
// `verifying`. Resolved once, only on the branch that consumes it.
|
|
1278
|
-
|
|
1429
|
+
dispatchSignals = NO_DISPATCH_SIGNALS) {
|
|
1279
1430
|
const now = ctx.deps.now();
|
|
1280
1431
|
let next = state;
|
|
1281
1432
|
let slot = slotOf(next, unit);
|
|
@@ -1289,11 +1440,10 @@ lastTool = null) {
|
|
|
1289
1440
|
// hold the exit in `verifying` until truth returns, then decide. The agent
|
|
1290
1441
|
// is already gone; no slot work is lost by waiting.
|
|
1291
1442
|
if (!truth.reachable) {
|
|
1292
|
-
|
|
1443
|
+
return journalGroundTruthUnreachableIfDue(ctx, next, unit, {
|
|
1293
1444
|
slot: slot.id,
|
|
1294
1445
|
detail: 'exit verification paused until truth returns',
|
|
1295
1446
|
});
|
|
1296
|
-
return next;
|
|
1297
1447
|
}
|
|
1298
1448
|
const issue = (0, journal_1.issueOfUnit)(unit);
|
|
1299
1449
|
const entry = issue !== null ? (0, state_1.findEntry)(next, issue) : undefined;
|
|
@@ -1301,12 +1451,12 @@ lastTool = null) {
|
|
|
1301
1451
|
// takes the unit (AC2's "never inferred from agent exit" cut both ways:
|
|
1302
1452
|
// the park IS the milestone, the merge is not).
|
|
1303
1453
|
if (entry !== undefined && entry.status === 'dispatched' && (0, groundtruth_1.isParkedMilestone)(truth.milestone)) {
|
|
1304
|
-
// A verified park is also proof dispatch is healthy (#505) — reset
|
|
1305
|
-
//
|
|
1306
|
-
// healthy park sandwiched between two unrelated units' suspect
|
|
1307
|
-
// would be invisible to the
|
|
1308
|
-
//
|
|
1309
|
-
const parked = recordDispatchOutcome(ctx, next, unit, slot, false);
|
|
1454
|
+
// A verified park is also proof dispatch is healthy (#505/#629) — reset
|
|
1455
|
+
// both streaks exactly like the sibling `completeUnit` branch below, or a
|
|
1456
|
+
// healthy park sandwiched between two unrelated units' suspect/api-error
|
|
1457
|
+
// exits would be invisible to the correlation and could still tip it
|
|
1458
|
+
// into a false-positive pause.
|
|
1459
|
+
const parked = (0, dispatch_health_1.resetDispatchApiErrorStreak)(recordDispatchOutcome(ctx, next, unit, slot, false));
|
|
1310
1460
|
const pr = (0, groundtruth_1.prOfMilestone)(truth.milestone); // non-null: isParkedMilestone guarantees it
|
|
1311
1461
|
if (pr === null) {
|
|
1312
1462
|
// Structurally unreachable, and deliberately not silent if it ever is:
|
|
@@ -1314,15 +1464,24 @@ lastTool = null) {
|
|
|
1314
1464
|
// journaled leaves it re-deciding the same way every tick, forever,
|
|
1315
1465
|
// against an empty trail. Return the recorded state (the pre-#596 form
|
|
1316
1466
|
// did — `parkUnit`'s internal guard returned the state it was handed),
|
|
1317
|
-
// not the un-recorded `next`.
|
|
1318
|
-
|
|
1467
|
+
// not the un-recorded `next`. #632: dedup like the sibling check above
|
|
1468
|
+
// — same marker, since only one of the two can be live for this unit
|
|
1469
|
+
// on a given tick (this branch is reached only after `truth.reachable`
|
|
1470
|
+
// already held).
|
|
1471
|
+
return journalGroundTruthUnreachableIfDue(ctx, parked, unit, {
|
|
1319
1472
|
slot: slot.id,
|
|
1320
1473
|
detail: `parked milestone (run=${truth.milestone?.run ?? 'unknown'}) carries no parseable pr= key — holding in verifying`,
|
|
1321
1474
|
});
|
|
1322
|
-
return parked;
|
|
1323
1475
|
}
|
|
1324
|
-
|
|
1476
|
+
// #632: a pr= key was found — both flavors of this unit's
|
|
1477
|
+
// ground-truth-unreachable streak (unreachable poll, unparseable pr=)
|
|
1478
|
+
// are resolved.
|
|
1479
|
+
return parkUnit(ctx, clearGroundTruthUnreachable(parked, unit, now), unit, pr);
|
|
1325
1480
|
}
|
|
1481
|
+
// #632: truth answered this tick and this unit isn't stuck on the
|
|
1482
|
+
// missing-pr= edge above — any streak recorded against a PREVIOUS tick
|
|
1483
|
+
// (either flavor) is over.
|
|
1484
|
+
next = clearGroundTruthUnreachable(next, unit, now);
|
|
1326
1485
|
// #575: fence to THIS dispatch's `spawned_at` — an agent that exited having
|
|
1327
1486
|
// posted nothing new must not read as complete against the issue's
|
|
1328
1487
|
// PREVIOUS run's report milestone; it falls through to the recovery ladder
|
|
@@ -1331,18 +1490,30 @@ lastTool = null) {
|
|
|
1331
1490
|
const verifiedComplete = (0, groundtruth_1.isVerifiedComplete)(truth.milestone, closedSignal, slot.spawned_at);
|
|
1332
1491
|
next = journalStaleMilestoneIfIgnored(ctx, next, unit, slot, truth, verifiedComplete, closedSignal);
|
|
1333
1492
|
if (verifiedComplete) {
|
|
1334
|
-
|
|
1493
|
+
const completed = (0, dispatch_health_1.resetDispatchApiErrorStreak)(recordDispatchOutcome(ctx, next, unit, slot, false));
|
|
1494
|
+
return completeUnit(ctx, completed, unit, via);
|
|
1495
|
+
}
|
|
1496
|
+
const resolved = typeof dispatchSignals === 'function' ? dispatchSignals() : dispatchSignals;
|
|
1497
|
+
// #629: a CONFIRMED provider API error is a dispatch failure, never an
|
|
1498
|
+
// unverified exit — it skips `recordDispatchOutcome`'s timing-based
|
|
1499
|
+
// suspect check entirely (the classification is deterministic, not a
|
|
1500
|
+
// heuristic) and never reaches `enterRecovery`'s escalation.
|
|
1501
|
+
if (resolved.apiError) {
|
|
1502
|
+
// `journalFailure: false` — `enterRecovery` journals its own `causeEvent`
|
|
1503
|
+
// (`'dispatch-failure'`) with this same evidence immediately below; without
|
|
1504
|
+
// the flag this rail double-journals every confirmed error (#629 review).
|
|
1505
|
+
const recorded = (0, dispatch_health_1.recordDispatchApiError)((event, u, extra) => journal(ctx, event, u, extra), next, unit, resolved.apiError, { journalFailure: false });
|
|
1506
|
+
return enterRecovery(ctx, recorded, unit, 'dispatch-failure', (0, dispatch_health_1.dispatchApiErrorDetail)(resolved.apiError), truth, (0, dispatch_health_1.dispatchApiErrorFields)(resolved.apiError), { escalate: false });
|
|
1335
1507
|
}
|
|
1336
1508
|
const suspect = msSinceLastProgress(slot, now) < types_1.SUSPECT_DISPATCH_WINDOW_MS;
|
|
1337
1509
|
next = recordDispatchOutcome(ctx, next, unit, slot, suspect);
|
|
1338
|
-
const resolvedLastTool = typeof lastTool === 'function' ? lastTool() : lastTool;
|
|
1339
1510
|
return enterRecovery(ctx, next, unit, 'verify-incomplete', 'unverified-exit', truth, {
|
|
1340
1511
|
observed: truth.milestone
|
|
1341
1512
|
? `milestone ${truth.milestone.phase}/${truth.milestone.status}; closed=${truth.closed}`
|
|
1342
1513
|
: `no milestone; closed=${truth.closed}`,
|
|
1343
1514
|
// #591: attributes the unverified exit to a concrete cause (e.g. `Monitor`)
|
|
1344
1515
|
// without opening the transcript. Omitted when the log yielded no tool_use.
|
|
1345
|
-
...(
|
|
1516
|
+
...(resolved.lastTool !== null ? { last_tool: resolved.lastTool } : {}),
|
|
1346
1517
|
});
|
|
1347
1518
|
}
|
|
1348
1519
|
/** Re-attach or spawn a slot left `assigned` by a crash between assign and spawn. */
|
|
@@ -1361,6 +1532,14 @@ function reconcileAssigned(ctx, state, slot, unit) {
|
|
|
1361
1532
|
}
|
|
1362
1533
|
/** Reconcile a `recovering` slot: respawn with the escalated tier. */
|
|
1363
1534
|
function reconcileRecovering(ctx, state, unit) {
|
|
1535
|
+
// #629: while paused, do not resume a `recovering` slot's respawn — this is
|
|
1536
|
+
// the crash-recovery rail (a sched restart caught a slot between
|
|
1537
|
+
// `enterRecovery`'s transition and its own `spawnUnit` call, OR `enterRecovery`
|
|
1538
|
+
// itself deliberately parked an unescalated redispatch here, see its own
|
|
1539
|
+
// pause check). Either way, `sched resume` is what lets the tick loop reach
|
|
1540
|
+
// this function again and actually respawn.
|
|
1541
|
+
if (state.paused)
|
|
1542
|
+
return state;
|
|
1364
1543
|
return spawnUnit(ctx, state, unit);
|
|
1365
1544
|
}
|
|
1366
1545
|
// --- Tick phases ---
|
|
@@ -1402,7 +1581,7 @@ function reconcileSlots(ctx, state, polled) {
|
|
|
1402
1581
|
// whatever `reconcileRunning` saw. Passed as a thunk so the read
|
|
1403
1582
|
// happens only on the tick that actually reaches the unverified-exit
|
|
1404
1583
|
// decision, not on every tick an outage holds the slot here.
|
|
1405
|
-
next = completeUnitOrRecover(ctx, next, unit, truth, 'verify-complete', () =>
|
|
1584
|
+
next = completeUnitOrRecover(ctx, next, unit, truth, 'verify-complete', () => readDispatchSignalsForSlot(ctx, slot, unit));
|
|
1406
1585
|
break;
|
|
1407
1586
|
case 'recovering':
|
|
1408
1587
|
next = reconcileRecovering(ctx, next, unit);
|
|
@@ -1422,7 +1601,12 @@ function requeueOrphanedDispatches(ctx, state) {
|
|
|
1422
1601
|
continue;
|
|
1423
1602
|
const now = ctx.deps.now();
|
|
1424
1603
|
next = (0, state_1.transitionIssue)(next, entry.issue, 'blocked', { reason: 'orphaned-dispatch' }, now);
|
|
1425
|
-
|
|
1604
|
+
// #633: same rule as `requeueMember` — a requeue is a fresh attempt, so a
|
|
1605
|
+
// dedup streak recorded against the PREVIOUS dispatch must not suppress
|
|
1606
|
+
// that condition's first occurrence on this one. Without it, a crash
|
|
1607
|
+
// during an outage leaves `ground_truth_unreachable_ticks` mid-streak and
|
|
1608
|
+
// the redispatched unit stays silent until the next re-announcement.
|
|
1609
|
+
next = (0, state_1.transitionIssue)(next, entry.issue, 'queued', { reason: null, ...state_1.CLEARED_ENTRY_DEDUP_MARKERS }, now);
|
|
1426
1610
|
journal(ctx, 'requeued', `issue:${entry.issue}`, {
|
|
1427
1611
|
detail: 'orphaned dispatch requeued after restart',
|
|
1428
1612
|
});
|
|
@@ -1457,14 +1641,18 @@ function reconcileParked(ctx, state, prPoll) {
|
|
|
1457
1641
|
if (!prPoll.truths.has(issue)) {
|
|
1458
1642
|
continue; // parked AFTER the poll ran (this tick) — next cadence picks it up
|
|
1459
1643
|
}
|
|
1460
|
-
|
|
1644
|
+
next = journalGroundTruthUnreachableIfDue(ctx, next, unit, {
|
|
1461
1645
|
detail: 'pr watch paused until truth returns',
|
|
1462
1646
|
});
|
|
1463
1647
|
continue;
|
|
1464
1648
|
}
|
|
1649
|
+
// #632: truth answered this tick — any unreachable streak recorded
|
|
1650
|
+
// against a PREVIOUS tick is over.
|
|
1651
|
+
next = clearGroundTruthUnreachable(next, unit, ctx.deps.now());
|
|
1465
1652
|
const failWatch = (reason) => {
|
|
1466
1653
|
journal(ctx, 'pr-watch-failed', unit, { reason, pr: entry.pr });
|
|
1467
|
-
|
|
1654
|
+
// #632: the watch is ending (terminal failure) — no more "waiting".
|
|
1655
|
+
return clearPrWatchWaiting(failUnit(ctx, next, unit, reason), unit, ctx.deps.now());
|
|
1468
1656
|
};
|
|
1469
1657
|
// #501: MERGED is checked FIRST, before any failure rail. A PR that is
|
|
1470
1658
|
// genuinely merged is merged regardless of a leftover `auto-merge-blocked`
|
|
@@ -1478,7 +1666,7 @@ function reconcileParked(ctx, state, prPoll) {
|
|
|
1478
1666
|
// AC1: the issue must ALSO be closed (a merged PR auto-closes it) —
|
|
1479
1667
|
// until GitHub propagates, the unit stays parked and keeps watching.
|
|
1480
1668
|
if (prPoll.closed.get(issue) !== true) {
|
|
1481
|
-
|
|
1669
|
+
next = journalPrWatchWaitingIfDue(ctx, next, unit, {
|
|
1482
1670
|
pr: entry.pr,
|
|
1483
1671
|
mergedAt: truth.mergedAt,
|
|
1484
1672
|
detail: 'merge seen but issue not closed — keep watching',
|
|
@@ -1486,6 +1674,7 @@ function reconcileParked(ctx, state, prPoll) {
|
|
|
1486
1674
|
continue;
|
|
1487
1675
|
}
|
|
1488
1676
|
next = (0, state_1.transitionIssue)(next, issue, 'shipped', { reason: null }, ctx.deps.now());
|
|
1677
|
+
next = clearPrWatchWaiting(next, unit, ctx.deps.now());
|
|
1489
1678
|
journal(ctx, 'merge-accepted', unit, { pr: entry.pr, mergedAt: truth.mergedAt });
|
|
1490
1679
|
ctx.result.mergeAccepted.push(unit);
|
|
1491
1680
|
continue;
|
|
@@ -1501,7 +1690,12 @@ function reconcileParked(ctx, state, prPoll) {
|
|
|
1501
1690
|
if (truth.state === 'CLOSED' && truth.mergedAt === null) {
|
|
1502
1691
|
next = failWatch('pr-closed-unmerged');
|
|
1503
1692
|
}
|
|
1504
|
-
|
|
1693
|
+
else {
|
|
1694
|
+
// OPEN (or mergeable UNKNOWN) — keep watching. #632: this tick's truth
|
|
1695
|
+
// is NOT "merge seen but not closed", so a waiting streak from an
|
|
1696
|
+
// earlier tick's merge-then-reverted flicker is over.
|
|
1697
|
+
next = clearPrWatchWaiting(next, unit, ctx.deps.now());
|
|
1698
|
+
}
|
|
1505
1699
|
}
|
|
1506
1700
|
return next;
|
|
1507
1701
|
}
|
|
@@ -1537,15 +1731,23 @@ function reconcileStaleFailedParks(ctx, state, prPoll) {
|
|
|
1537
1731
|
if (!prPoll.truths.has(issue)) {
|
|
1538
1732
|
continue; // failed AFTER the poll ran this tick — next cadence picks it up
|
|
1539
1733
|
}
|
|
1540
|
-
|
|
1734
|
+
next = journalGroundTruthUnreachableIfDue(ctx, next, unit, {
|
|
1541
1735
|
detail: 'stale-failure reconcile paused until truth returns',
|
|
1542
1736
|
});
|
|
1543
1737
|
continue;
|
|
1544
1738
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1739
|
+
// #632: truth answered this tick — any unreachable streak recorded
|
|
1740
|
+
// against a PREVIOUS tick is over.
|
|
1741
|
+
next = clearGroundTruthUnreachable(next, unit, ctx.deps.now());
|
|
1742
|
+
if (!isPrMerged(truth)) {
|
|
1743
|
+
// Still blocked/open/conflicting — stays failed. #632: not "merge seen
|
|
1744
|
+
// but not closed" either, so a waiting streak from an earlier tick's
|
|
1745
|
+
// merge-then-reverted flicker is over.
|
|
1746
|
+
next = clearPrWatchWaiting(next, unit, ctx.deps.now());
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1547
1749
|
if (prPoll.closed.get(issue) !== true) {
|
|
1548
|
-
|
|
1750
|
+
next = journalPrWatchWaitingIfDue(ctx, next, unit, {
|
|
1549
1751
|
pr: entry.pr,
|
|
1550
1752
|
mergedAt: truth.mergedAt,
|
|
1551
1753
|
reason: AUTO_MERGE_BLOCKED_REASON,
|
|
@@ -1555,6 +1757,7 @@ function reconcileStaleFailedParks(ctx, state, prPoll) {
|
|
|
1555
1757
|
}
|
|
1556
1758
|
const failedAt = entry.updated_at;
|
|
1557
1759
|
next = (0, state_1.transitionIssue)(next, issue, 'shipped', { reason: null }, ctx.deps.now());
|
|
1760
|
+
next = clearPrWatchWaiting(next, unit, ctx.deps.now());
|
|
1558
1761
|
journal(ctx, 'stale-failure-reconciled', unit, {
|
|
1559
1762
|
pr: entry.pr,
|
|
1560
1763
|
mergedAt: truth.mergedAt,
|
|
@@ -1656,6 +1859,17 @@ function runTeardownFor(deps, issue) {
|
|
|
1656
1859
|
const info = deps.groundTruth.setupInfo(issue);
|
|
1657
1860
|
const unit = `issue:${issue}`;
|
|
1658
1861
|
if (info === undefined) {
|
|
1862
|
+
// #633: AUDITED, NOT DEDUPED. This is a tenth site with #632's shape —
|
|
1863
|
+
// `teardownPendingIssues` reaches it every tick and a `null` return leaves
|
|
1864
|
+
// `cleanup` unset, so an unreachable `setupInfo` re-emits this line once
|
|
1865
|
+
// per reconcile interval for as long as the outage lasts.
|
|
1866
|
+
//
|
|
1867
|
+
// Left as-is deliberately: #632 enumerated nine sites and its plan makes
|
|
1868
|
+
// the boundary load-bearing ("a site outside the table is a NEW issue, not
|
|
1869
|
+
// a reason to widen this one") — widening is what dissolved b-07. The fix
|
|
1870
|
+
// is tracked in #636, and is not a one-liner here: `runTeardownFor` runs
|
|
1871
|
+
// OUTSIDE the store lock and has no `SchedState` to patch, so the marker
|
|
1872
|
+
// has to be threaded through the caller's second lock pass.
|
|
1659
1873
|
deps.journal.append((0, journal_1.unitEvent)('ground-truth-unreachable', unit, {
|
|
1660
1874
|
detail: 'teardown paused until truth returns',
|
|
1661
1875
|
}), deps.now());
|