@ai-dossier/sched 0.48.1 → 0.50.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 +94 -7
- package/dist/anchor-close.d.ts +160 -0
- package/dist/anchor-close.d.ts.map +1 -1
- package/dist/anchor-close.js +319 -36
- package/dist/anchor-close.js.map +1 -1
- package/dist/batch-dispatch.d.ts +58 -0
- package/dist/batch-dispatch.d.ts.map +1 -1
- package/dist/batch-dispatch.js +174 -41
- package/dist/batch-dispatch.js.map +1 -1
- package/dist/groundtruth.d.ts +63 -3
- package/dist/groundtruth.d.ts.map +1 -1
- package/dist/groundtruth.js +115 -14
- package/dist/groundtruth.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/labels.d.ts +9 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +10 -1
- package/dist/labels.js.map +1 -1
- package/dist/state.d.ts +12 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +47 -21
- package/dist/state.js.map +1 -1
- package/dist/status.d.ts +33 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +20 -1
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +20 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/batch-dispatch.js
CHANGED
|
@@ -99,6 +99,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
99
99
|
exports.memberBranchFor = memberBranchFor;
|
|
100
100
|
exports.evictMemberAndContinue = evictMemberAndContinue;
|
|
101
101
|
exports.resumeBlockedGate = resumeBlockedGate;
|
|
102
|
+
exports.reconcileStaleBlockedBatches = reconcileStaleBlockedBatches;
|
|
102
103
|
exports.memberDispatchModeFor = memberDispatchModeFor;
|
|
103
104
|
exports.runBatchTick = runBatchTick;
|
|
104
105
|
const fs = __importStar(require("node:fs"));
|
|
@@ -2655,18 +2656,44 @@ function reconcileReportSlot(deps, batchId, slot, now, result) {
|
|
|
2655
2656
|
}
|
|
2656
2657
|
// --- PR watch for `awaiting-merge` batches (mirrors `pollParkedPrs`/`reconcileParked`) ---
|
|
2657
2658
|
/**
|
|
2658
|
-
*
|
|
2659
|
-
* streak is recorded
|
|
2660
|
-
*
|
|
2659
|
+
* Clear one `<prefix>_reason`/`_since`/`_ticks` batch dedup marker — a no-op
|
|
2660
|
+
* when no streak is recorded (`currentReason === null`), so every caller is
|
|
2661
|
+
* safe to invoke unconditionally, every tick, on a batch that may or may not
|
|
2662
|
+
* have an open streak. Shared by `pr-watch-failed` (#630/#633),
|
|
2663
|
+
* `anchor-close-failed` (#768) and `pr-detect-ambiguous` (#789) — each
|
|
2664
|
+
* family previously carried its own copy of this exact body (#789 review).
|
|
2661
2665
|
*/
|
|
2662
|
-
function
|
|
2663
|
-
if (
|
|
2666
|
+
function clearBatchDedupMarker(deps, batch, currentReason, clearedFields, now) {
|
|
2667
|
+
if (currentReason === null)
|
|
2664
2668
|
return;
|
|
2665
2669
|
deps.store.withLock((s) => ({
|
|
2666
|
-
state: (0, state_1.patchBatch)(s, batch.id,
|
|
2670
|
+
state: (0, state_1.patchBatch)(s, batch.id, clearedFields, now, false),
|
|
2667
2671
|
result: undefined,
|
|
2668
2672
|
}));
|
|
2669
2673
|
}
|
|
2674
|
+
/**
|
|
2675
|
+
* Advance one `<prefix>_reason`/`_since`/`_ticks` batch dedup marker by one
|
|
2676
|
+
* tick and decide whether THIS tick should journal — the streak's first tick,
|
|
2677
|
+
* or every `JOURNAL_DEDUP_REANNOUNCE_TICKS` after, never every tick
|
|
2678
|
+
* (#610/#630/#632/#768/#789's shared shape). Pure — the caller still owns
|
|
2679
|
+
* building its own journal payload (each family logs different extra fields)
|
|
2680
|
+
* and the `deps.store.withLock` persistence write; this only computes
|
|
2681
|
+
* `since`/`ticks`/whether to announce.
|
|
2682
|
+
*/
|
|
2683
|
+
function advanceBatchDedupStreak(priorReason, priorSince, priorTicks, reason, now) {
|
|
2684
|
+
const isNewStreak = priorReason !== reason || priorSince === null;
|
|
2685
|
+
const ticks = isNewStreak ? 1 : priorTicks + 1;
|
|
2686
|
+
const since = isNewStreak || priorSince === null ? now.toISOString() : priorSince;
|
|
2687
|
+
return { since, ticks, announce: isNewStreak || ticks % types_1.JOURNAL_DEDUP_REANNOUNCE_TICKS === 0 };
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* Reset a batch's `pr-watch-failed` dedup marker (#630/#633). A no-op when no
|
|
2691
|
+
* streak is recorded, so it is safe to call on every non-`awaiting-merge`
|
|
2692
|
+
* batch every tick.
|
|
2693
|
+
*/
|
|
2694
|
+
function clearPrWatchFailed(deps, batch, now) {
|
|
2695
|
+
clearBatchDedupMarker(deps, batch, batch.pr_watch_failed_reason, state_1.CLEARED_PR_WATCH_FIELDS, now);
|
|
2696
|
+
}
|
|
2670
2697
|
function reconcilePrWatch(deps, now, result) {
|
|
2671
2698
|
const state = deps.store.load();
|
|
2672
2699
|
for (const batch of state.batches) {
|
|
@@ -2715,9 +2742,6 @@ function reconcilePrWatch(deps, now, result) {
|
|
|
2715
2742
|
}
|
|
2716
2743
|
if (truth.blocked || truth.mergeable === 'CONFLICTING') {
|
|
2717
2744
|
const reason = truth.blocked ? 'auto-merge-blocked' : 'pr-conflicting';
|
|
2718
|
-
const isNewStreak = batch.pr_watch_failed_reason !== reason;
|
|
2719
|
-
const ticks = isNewStreak ? 1 : batch.pr_watch_failed_ticks + 1;
|
|
2720
|
-
const since = isNewStreak ? now.toISOString() : batch.pr_watch_failed_since;
|
|
2721
2745
|
// #630: journal on the streak's first tick, then again only every
|
|
2722
2746
|
// `JOURNAL_DEDUP_REANNOUNCE_TICKS` — never every tick — mirrors #610's
|
|
2723
2747
|
// `stale_milestone_ignored_for` dedup for the "distinct condition"
|
|
@@ -2729,7 +2753,8 @@ function reconcilePrWatch(deps, now, result) {
|
|
|
2729
2753
|
// of its own); `since` is the streak's onset, and is what makes the
|
|
2730
2754
|
// duration legible — `ticks_persisted` is a tick count against an
|
|
2731
2755
|
// operator-tunable `reconcile_interval_ms`, not a fixed wall-clock.
|
|
2732
|
-
|
|
2756
|
+
const { since, ticks, announce } = advanceBatchDedupStreak(batch.pr_watch_failed_reason, batch.pr_watch_failed_since, batch.pr_watch_failed_ticks, reason, now);
|
|
2757
|
+
if (announce) {
|
|
2733
2758
|
journalEvent(deps, 'pr-watch-failed', unit(batch.id), {
|
|
2734
2759
|
reason,
|
|
2735
2760
|
pr,
|
|
@@ -2872,6 +2897,51 @@ function branchMergedIntoBase(deps, batch) {
|
|
|
2872
2897
|
const baseTip = deps.exec('git', ['rev-parse', base], deps.repoDir);
|
|
2873
2898
|
return branchTip !== null && baseTip !== null && branchTip.trim() !== '' && branchTip !== baseTip;
|
|
2874
2899
|
}
|
|
2900
|
+
/**
|
|
2901
|
+
* The ambiguity condition stopped qualifying (the branch resolved to zero or
|
|
2902
|
+
* exactly one candidate), or the batch left `blocked` some other way (this
|
|
2903
|
+
* reconcile's own success, `sched resume --batch`, `sched abandon`, or the
|
|
2904
|
+
* batch aging out of the reconcile window): end any `pr-detect-ambiguous`
|
|
2905
|
+
* streak, so a future ambiguity episode — even a re-block of the SAME batch
|
|
2906
|
+
* with the SAME candidates — journals its own first line instead of silently
|
|
2907
|
+
* continuing a streak from a previous `blocked` stretch (#789 review). A
|
|
2908
|
+
* no-op (no write) when no streak is recorded.
|
|
2909
|
+
*/
|
|
2910
|
+
function clearPrDetectAmbiguous(deps, batch, now) {
|
|
2911
|
+
clearBatchDedupMarker(deps, batch, batch.pr_detect_ambiguous_reason, state_1.CLEARED_PR_DETECT_AMBIGUOUS_FIELDS, now);
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* #789: more than one MERGED PR matches `batch.branch` — positive evidence
|
|
2915
|
+
* only, so the caller records nothing. Journal it on the streak's first tick
|
|
2916
|
+
* and every `JOURNAL_DEDUP_REANNOUNCE_TICKS` after — never every tick — the
|
|
2917
|
+
* same dedup shape `recordAnchorCloseFailed`/`reconcilePrWatch` use.
|
|
2918
|
+
*/
|
|
2919
|
+
function recordPrDetectAmbiguous(deps, batch, matches, now) {
|
|
2920
|
+
const { since, ticks, announce } = advanceBatchDedupStreak(batch.pr_detect_ambiguous_reason, batch.pr_detect_ambiguous_since, batch.pr_detect_ambiguous_ticks, state_1.PR_DETECT_AMBIGUOUS_REASON, now);
|
|
2921
|
+
if (announce) {
|
|
2922
|
+
journalEvent(deps, 'pr-detect-ambiguous', unit(batch.id), {
|
|
2923
|
+
reason: state_1.PR_DETECT_AMBIGUOUS_REASON,
|
|
2924
|
+
branch: batch.branch ?? undefined,
|
|
2925
|
+
matches: matches.join(','),
|
|
2926
|
+
at: now.toISOString(),
|
|
2927
|
+
since,
|
|
2928
|
+
ticks_persisted: ticks,
|
|
2929
|
+
// #789 review: neither "record it explicitly" (no command exists) nor
|
|
2930
|
+
// "close the extras" (a MERGED PR cannot be closed, so it never leaves
|
|
2931
|
+
// `gh pr list --state merged`) was ever an actionable remedy — state
|
|
2932
|
+
// what actually resolves it instead. #824 tracks the missing verb.
|
|
2933
|
+
detail: `${matches.length} MERGED PRs match head=${batch.branch ?? '?'} (#${matches.join(', #')}) — refusing to guess which is ours; nothing recorded. The batch still reconciles via commits-in-base/members-closed evidence if either holds; otherwise inspect and \`sched abandon --batch ${batch.id}\`, or use \`sched attach-pr\` once #824 ships it`,
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
deps.store.withLock((s) => ({
|
|
2937
|
+
state: (0, state_1.patchBatch)(s, batch.id, {
|
|
2938
|
+
pr_detect_ambiguous_reason: state_1.PR_DETECT_AMBIGUOUS_REASON,
|
|
2939
|
+
pr_detect_ambiguous_since: since,
|
|
2940
|
+
pr_detect_ambiguous_ticks: ticks,
|
|
2941
|
+
}, now, false),
|
|
2942
|
+
result: undefined,
|
|
2943
|
+
}));
|
|
2944
|
+
}
|
|
2875
2945
|
/**
|
|
2876
2946
|
* #686: reconcile a `blocked` batch whose work demonstrably shipped anyway.
|
|
2877
2947
|
* `reconcilePrWatch` above only watches `awaiting-merge` batches, and
|
|
@@ -2884,21 +2954,28 @@ function branchMergedIntoBase(deps, batch) {
|
|
|
2884
2954
|
* (engine.ts) under the same principle: ground truth beats the ledger, for
|
|
2885
2955
|
* ANY stale verdict — the blocked_reason is deliberately not examined.
|
|
2886
2956
|
*
|
|
2887
|
-
* Evidence (
|
|
2888
|
-
*
|
|
2889
|
-
* batch
|
|
2890
|
-
*
|
|
2891
|
-
*
|
|
2957
|
+
* Evidence (cheapest first): the batch PR is MERGED with a real timestamp (a
|
|
2958
|
+
* merged batch PR contains the whole batch branch) — including one
|
|
2959
|
+
* `batch.pr` never recorded at all (#789): when `pr === null`, before falling
|
|
2960
|
+
* through to the weaker branch/members evidence below, look for exactly one
|
|
2961
|
+
* MERGED PR whose head is the batch branch (`imboard#4255`, a hand-opened PR
|
|
2962
|
+
* the ledger never saw). Positive evidence only — zero candidates changes
|
|
2963
|
+
* nothing, more than one is `ambiguous` and journaled rather than guessed —
|
|
2964
|
+
* or the batch branch has fully landed in the base branch. A survivability
|
|
2965
|
+
* guard applies to every path (see `everySurvivingMemberDispatched`) — a
|
|
2966
|
+
* batch with a surviving member that was never dispatched is not
|
|
2967
|
+
* reconcilable.
|
|
2892
2968
|
*
|
|
2893
2969
|
* On reconcile the batch joins the SAME rail a normally-merged batch takes:
|
|
2894
2970
|
* `blocked → merged → deployed` (the `blocked → merged` edge exists for
|
|
2895
2971
|
* exactly this caller), surviving members walked to `done` along legal
|
|
2896
|
-
* edges, and — when
|
|
2897
|
-
*
|
|
2898
|
-
*
|
|
2899
|
-
* PR), so the same lock continues
|
|
2900
|
-
*
|
|
2901
|
-
* agent was dispatched rather than
|
|
2972
|
+
* edges, and — when the batch now carries a PR (recorded before or detected
|
|
2973
|
+
* here) — the ordinary `deployed` machinery dispatches the report agent next
|
|
2974
|
+
* tick, with teardown after `batch-report done`. With no PR at all no report
|
|
2975
|
+
* agent can be prompted (it names the PR), so the same lock continues
|
|
2976
|
+
* `deployed → reported → done` and the worktree is torn down here instead;
|
|
2977
|
+
* the journal line says why no report agent was dispatched rather than
|
|
2978
|
+
* leaving the gap silent.
|
|
2902
2979
|
*
|
|
2903
2980
|
* Bounded by construction: only `blocked` batches within the window are
|
|
2904
2981
|
* examined, the PR check is one `gh` call, git runs only when PR evidence
|
|
@@ -2910,19 +2987,48 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2910
2987
|
const state = deps.store.load();
|
|
2911
2988
|
const nowMs = now.getTime();
|
|
2912
2989
|
for (const batch of state.batches) {
|
|
2913
|
-
if (batch.status !== 'blocked')
|
|
2990
|
+
if (batch.status !== 'blocked') {
|
|
2991
|
+
// #789 review: the marker is scoped to ONE `blocked` stretch, exactly
|
|
2992
|
+
// like `pr_watch_failed_*`'s own `awaiting-merge` scoping (#633) — the
|
|
2993
|
+
// batch left `blocked` (this reconcile's own success below, `sched
|
|
2994
|
+
// resume --batch`, or `sched abandon`), so a LATER re-block — even of
|
|
2995
|
+
// the SAME batch with the SAME candidate PRs — must journal a fresh
|
|
2996
|
+
// first line, never silently continue a stale streak from before.
|
|
2997
|
+
clearPrDetectAmbiguous(deps, batch, now);
|
|
2914
2998
|
continue;
|
|
2999
|
+
}
|
|
2915
3000
|
if (nowMs - Date.parse(batch.updated_at) >= STALE_BLOCKED_RECONCILE_WINDOW_MS)
|
|
2916
3001
|
continue;
|
|
2917
3002
|
if (!everySurvivingMemberDispatched(state, batch))
|
|
2918
3003
|
continue;
|
|
2919
3004
|
let evidence = null;
|
|
3005
|
+
// #789 review: one outer variable for the whole lookup outcome (rather
|
|
3006
|
+
// than three separate booleans/values re-deriving pieces of it) — the
|
|
3007
|
+
// resolution block below reads `lookup.kind` directly instead of a
|
|
3008
|
+
// parallel flag that has to be kept in sync with it by hand.
|
|
3009
|
+
let lookup;
|
|
2920
3010
|
if (batch.pr !== null) {
|
|
2921
3011
|
const truth = deps.groundTruth.prState(batch.pr);
|
|
2922
3012
|
if (truth !== undefined && truth.state === 'MERGED' && truth.mergedAt !== null) {
|
|
2923
3013
|
evidence = { kind: 'pr-merged', pr: batch.pr, mergedAt: truth.mergedAt };
|
|
2924
3014
|
}
|
|
2925
3015
|
}
|
|
3016
|
+
else if (batch.branch !== null) {
|
|
3017
|
+
// #789: batch.pr was never recorded — a hand-opened or manually
|
|
3018
|
+
// recovered PR the ledger never saw (the imboard#4255 shape). Look for
|
|
3019
|
+
// exactly one MERGED PR whose head is the batch branch before falling
|
|
3020
|
+
// through to the weaker branch/members evidence below. Positive
|
|
3021
|
+
// evidence only: `undefined` (unreachable) touches nothing, and
|
|
3022
|
+
// `ambiguous` (more than one candidate) is journaled, never guessed.
|
|
3023
|
+
lookup = deps.groundTruth.mergedPrForBranch?.(batch.branch, batch.base_branch, batch.created_at);
|
|
3024
|
+
if (lookup?.kind === 'found') {
|
|
3025
|
+
evidence = { kind: 'pr-merged', pr: lookup.pr, mergedAt: lookup.mergedAt };
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
// Set only when THIS tick's automatic lookup found the PR — never when
|
|
3029
|
+
// `batch.pr` was already recorded (that PR is reported via `evidence.pr`
|
|
3030
|
+
// as before, and there is nothing new to persist).
|
|
3031
|
+
const detectedPr = lookup?.kind === 'found' ? lookup.pr : null;
|
|
2926
3032
|
if (evidence === null && branchMergedIntoBase(deps, batch)) {
|
|
2927
3033
|
evidence = { kind: 'commits-in-base' };
|
|
2928
3034
|
}
|
|
@@ -2940,12 +3046,20 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2940
3046
|
}).kind === 'closable') {
|
|
2941
3047
|
evidence = { kind: 'members-closed' };
|
|
2942
3048
|
}
|
|
3049
|
+
// #789: resolve the ambiguity streak exactly once per tick. An ambiguous
|
|
3050
|
+
// lookup records only when the batch ISN'T reconciling by other evidence
|
|
3051
|
+
// this same tick; any evidence at all (including a `found`/`none` lookup
|
|
3052
|
+
// result) clears a stale or same-tick streak — reconciling via ANY
|
|
3053
|
+
// evidence means the batch is leaving `blocked`, so a same-tick ambiguity
|
|
3054
|
+
// is moot too.
|
|
3055
|
+
if (lookup?.kind === 'ambiguous' && evidence === null) {
|
|
3056
|
+
recordPrDetectAmbiguous(deps, batch, lookup.matches, now);
|
|
3057
|
+
}
|
|
3058
|
+
else if (evidence !== null || lookup?.kind === 'found' || lookup?.kind === 'none') {
|
|
3059
|
+
clearPrDetectAmbiguous(deps, batch, now);
|
|
3060
|
+
}
|
|
2943
3061
|
if (evidence === null)
|
|
2944
3062
|
continue;
|
|
2945
|
-
// A members-closed reconcile has no merged batch PR for a report agent to
|
|
2946
|
-
// report on, whatever `batch.pr` recorded — finish the rail inline, as
|
|
2947
|
-
// for `pr === null`.
|
|
2948
|
-
const finishInline = batch.pr === null || evidence.kind === 'members-closed';
|
|
2949
3063
|
// ...but never tear its worktree down: the members shipped OUTSIDE the
|
|
2950
3064
|
// batch branch, so the blocked integration worktree may hold an
|
|
2951
3065
|
// operator's unpushed repair work. It is left in place, inert, and the
|
|
@@ -2955,6 +3069,12 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2955
3069
|
const failedAt = batch.updated_at;
|
|
2956
3070
|
let reconciled = false;
|
|
2957
3071
|
let unwalkable = [];
|
|
3072
|
+
// #789 review: what the lock ACTUALLY applied, not what the outer
|
|
3073
|
+
// snapshot predicted — re-derived from `b` (read under the lock) below,
|
|
3074
|
+
// since `batch.pr` could in principle have changed between the snapshot
|
|
3075
|
+
// above and the lock acquiring (e.g. a future `sched attach-pr`, #824).
|
|
3076
|
+
let effectivePr = null;
|
|
3077
|
+
let prWasRecorded = false;
|
|
2958
3078
|
deps.store.withLock((s) => {
|
|
2959
3079
|
const b = (0, state_1.findBatch)(s, batch.id);
|
|
2960
3080
|
// Re-check under the lock: `sched resume --batch` (a passing recheck)
|
|
@@ -2962,7 +3082,19 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2962
3082
|
// snapshot this pass started from.
|
|
2963
3083
|
if (!b || b.status !== 'blocked')
|
|
2964
3084
|
return { state: s, result: undefined };
|
|
2965
|
-
|
|
3085
|
+
// #789: record the auto-detected PR as part of the SAME transition that
|
|
3086
|
+
// leaves `blocked` — a batch whose ledger never had one now does. Only
|
|
3087
|
+
// when `b.pr` is STILL null under the lock — never overwrite a PR
|
|
3088
|
+
// something else recorded between the snapshot and here.
|
|
3089
|
+
prWasRecorded = detectedPr !== null && b.pr === null;
|
|
3090
|
+
effectivePr = prWasRecorded ? detectedPr : b.pr;
|
|
3091
|
+
let n = (0, state_1.transitionBatch)(s, batch.id, 'merged', {
|
|
3092
|
+
// The ambiguity streak (this tick's own, or a stale one) is moot
|
|
3093
|
+
// the instant the batch reconciles by any evidence — fold the
|
|
3094
|
+
// clear into this same transition instead of a separate lock write.
|
|
3095
|
+
...state_1.CLEARED_PR_DETECT_AMBIGUOUS_FIELDS,
|
|
3096
|
+
...(prWasRecorded ? { pr: detectedPr } : {}),
|
|
3097
|
+
}, now);
|
|
2966
3098
|
n = (0, state_1.transitionBatch)(n, batch.id, 'deployed', {}, now);
|
|
2967
3099
|
unwalkable = [];
|
|
2968
3100
|
for (const issue of b.members) {
|
|
@@ -2986,7 +3118,11 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2986
3118
|
entry = (0, state_1.findEntry)(n, issue);
|
|
2987
3119
|
}
|
|
2988
3120
|
}
|
|
2989
|
-
|
|
3121
|
+
// The batch now carries a PR — recorded before, recorded just above,
|
|
3122
|
+
// or still null — only when one exists does the ordinary `deployed`
|
|
3123
|
+
// machinery have anything to hand the report agent. A members-closed
|
|
3124
|
+
// reconcile has no merged batch PR to report on regardless.
|
|
3125
|
+
if (effectivePr === null || evidence.kind === 'members-closed') {
|
|
2990
3126
|
// No (merged) PR → no report agent is possible (its prompt names the
|
|
2991
3127
|
// PR). Finish the terminal rail and let teardown run below.
|
|
2992
3128
|
n = (0, state_1.transitionBatch)(n, batch.id, 'reported', {}, now);
|
|
@@ -2997,14 +3133,19 @@ function reconcileStaleBlockedBatches(deps, now, result, anchorCtx) {
|
|
|
2997
3133
|
});
|
|
2998
3134
|
if (!reconciled)
|
|
2999
3135
|
continue;
|
|
3136
|
+
const finishInline = effectivePr === null || evidence.kind === 'members-closed';
|
|
3000
3137
|
journalEvent(deps, 'stale-failure-reconciled', unit(batch.id), {
|
|
3001
|
-
pr:
|
|
3138
|
+
pr: effectivePr ?? undefined,
|
|
3002
3139
|
reason: blockedReason,
|
|
3003
3140
|
failedAt,
|
|
3004
3141
|
evidence: evidence.kind,
|
|
3005
3142
|
...(evidence.kind === 'pr-merged' ? { mergedAt: evidence.mergedAt } : {}),
|
|
3143
|
+
...(prWasRecorded ? { pr_detected: true } : {}),
|
|
3006
3144
|
detail: (evidence.kind === 'pr-merged'
|
|
3007
|
-
?
|
|
3145
|
+
? (prWasRecorded
|
|
3146
|
+
? `batch.pr was never recorded — detected PR #${evidence.pr} MERGED with head=${batch.branch} (gh pr list --state merged), recorded batch.pr=${evidence.pr}`
|
|
3147
|
+
: `PR #${evidence.pr} is MERGED`) +
|
|
3148
|
+
` — ledger reconciled blocked (${blockedReason}) to the merged rail (blocked at ${failedAt}); report and teardown will now dispatch`
|
|
3008
3149
|
: evidence.kind === 'members-closed'
|
|
3009
3150
|
? `every member issue is CLOSED as completed on GitHub with no failure trail — ledger reconciled blocked (${blockedReason}) to shipped (blocked at ${failedAt})`
|
|
3010
3151
|
: `every member commit is an ancestor of ${batch.base_branch} — ledger reconciled blocked (${blockedReason}) to shipped (blocked at ${failedAt})`) +
|
|
@@ -3221,12 +3362,7 @@ function recordAnchorClosed(deps, batchId, now) {
|
|
|
3221
3362
|
* stale streak's count. A no-op (no write) when no streak is recorded.
|
|
3222
3363
|
*/
|
|
3223
3364
|
function clearAnchorCloseFailed(deps, batch, now) {
|
|
3224
|
-
|
|
3225
|
-
return;
|
|
3226
|
-
deps.store.withLock((s) => ({
|
|
3227
|
-
state: (0, state_1.patchBatch)(s, batch.id, state_1.CLEARED_ANCHOR_CLOSE_FAILED_FIELDS, now, false),
|
|
3228
|
-
result: undefined,
|
|
3229
|
-
}));
|
|
3365
|
+
clearBatchDedupMarker(deps, batch, batch.anchor_close_failed_reason, state_1.CLEARED_ANCHOR_CLOSE_FAILED_FIELDS, now);
|
|
3230
3366
|
}
|
|
3231
3367
|
/**
|
|
3232
3368
|
* A close that qualified but failed: journal it on the streak's first tick and
|
|
@@ -3234,11 +3370,8 @@ function clearAnchorCloseFailed(deps, batch, now) {
|
|
|
3234
3370
|
* exactly `reconcilePrWatch`'s `pr-watch-failed` dedup.
|
|
3235
3371
|
*/
|
|
3236
3372
|
function recordAnchorCloseFailed(deps, batch, outcome, shipped, now) {
|
|
3237
|
-
const
|
|
3238
|
-
|
|
3239
|
-
const ticks = isNewStreak ? 1 : batch.anchor_close_failed_ticks + 1;
|
|
3240
|
-
const since = isNewStreak || priorSince === null ? now.toISOString() : priorSince;
|
|
3241
|
-
if (isNewStreak || ticks % types_1.JOURNAL_DEDUP_REANNOUNCE_TICKS === 0) {
|
|
3373
|
+
const { since, ticks, announce } = advanceBatchDedupStreak(batch.anchor_close_failed_reason, batch.anchor_close_failed_since, batch.anchor_close_failed_ticks, outcome, now);
|
|
3374
|
+
if (announce) {
|
|
3242
3375
|
journalEvent(deps, 'anchor-close-failed', unit(batch.id), {
|
|
3243
3376
|
issue: batch.anchor ?? undefined,
|
|
3244
3377
|
reason: outcome,
|