@ai-dossier/sched 0.56.0 → 0.57.1

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.
@@ -189,6 +189,44 @@ export declare function beginFixAttempt(state: SchedState, batchId: string, issu
189
189
  export declare function resolveFixAttempt(state: SchedState, batchId: string, issue: number, outcome: 'green' | 'red', deps: RecoveryDeps): {
190
190
  state: SchedState;
191
191
  };
192
+ /** One of a batch's member branches, as it exists on origin (#840). */
193
+ export interface RemoteMemberBranch {
194
+ /** `batch/<batchId>-m<index>-<issue>` */
195
+ branch: string;
196
+ index: number;
197
+ issue: number;
198
+ }
199
+ /**
200
+ * #840: this batch's member branches that exist on ORIGIN right now —
201
+ * `batch/<batchId>-m<n>-<issue>` (`memberBranchFor`'s shape), matched exactly
202
+ * so another batch whose id merely starts with this one's (`b1-a`, a halved
203
+ * split) or the integration branch (`batch/<id>-<date>`) never matches.
204
+ * Since #840 a member branch outlives its landing — it is deleted when the
205
+ * batch ships or dissolves (`teardownBatch`), so this is also how an
206
+ * aggregate-suite eviction finds the branch a landed member can be parked on.
207
+ * `null` when origin could not be read (never "no branches").
208
+ */
209
+ export declare function listRemoteMemberBranches(exec: ExecFn, repoDir: string, batchId: string): RemoteMemberBranch[] | null;
210
+ /** What {@link pruneMemberBranches} did (#840). */
211
+ export interface MemberBranchPrune {
212
+ deleted: string[];
213
+ /** Branches a live queue entry still continues from — never deleted. */
214
+ kept: string[];
215
+ /** Branches whose delete failed (left on origin). */
216
+ failed: string[];
217
+ }
218
+ /**
219
+ * #840: the batch is over — delete its member branches from origin. They
220
+ * outlive landing so an aggregate-suite eviction can park a landed member on
221
+ * its branch; once the batch ships or dissolves (`teardownBatch`), or is
222
+ * abandoned (`sched abandon --batch`), nothing of the batch needs them —
223
+ * except a branch a NON-terminal queue entry's `failure_evidence.branch`
224
+ * still names (a parked member, or one requeued to continue from it): that
225
+ * branch is the member's work and is kept. One `git push --delete` per ref,
226
+ * so one already-gone ref cannot mask the others' outcome. `null` when
227
+ * origin could not be listed (nothing deleted). Never throws.
228
+ */
229
+ export declare function pruneMemberBranches(exec: ExecFn, repoDir: string, state: SchedState, batchId: string): MemberBranchPrune | null;
192
230
  export interface EvictionInput {
193
231
  /** Members to evict; eviction-group partners are added automatically. */
194
232
  issues: readonly number[];
@@ -207,6 +245,12 @@ export interface EvictionOutcome {
207
245
  evicted: number[];
208
246
  /** Members actually put back on the queue (shipped members are not). */
209
247
  requeued: number[];
248
+ /**
249
+ * #840: members PARKED `evicted` on their still-live member branch instead
250
+ * of requeued — an operator's `sched requeue` continues them from it.
251
+ * Includes any a dissolve triggered by this eviction parked.
252
+ */
253
+ parked: number[];
210
254
  /** Commits reverted, in the order they were reverted (newest first). */
211
255
  reverted: string[];
212
256
  /** True when a revert conflicted — the batch dissolves instead. */
@@ -231,8 +275,9 @@ export interface EvictionOutcome {
231
275
  */
232
276
  export declare function expandEvictionGroups(batch: BatchEntry, issues: readonly number[]): number[];
233
277
  /**
234
- * `attributing → evicting`: revert the offending members' commits, requeue them
235
- * as full-cycle with their failure evidence, re-run the suite, and check the
278
+ * `attributing → evicting`: revert the offending members' commits, park each
279
+ * on its still-live member branch (#840; else requeue it as full-cycle) with
280
+ * its failure evidence, re-run the suite, and check the
236
281
  * batch's configurable dissolve threshold (`DissolvePolicy`, AC2/AC3/AC5).
237
282
  * Crossing it has two outcomes (#563): if the re-run suite came back green
238
283
  * for the survivors, the batch is PRESERVED — trimmed to its survivors and
@@ -357,7 +402,15 @@ export interface BlockOptions {
357
402
  export declare function blockBatch(state: SchedState, batchId: string, opts: BlockOptions, deps: RecoveryDeps): {
358
403
  state: SchedState;
359
404
  };
360
- export type PrConflictAction = 'reship' | 'dissolved';
405
+ /**
406
+ * `reship` — rebased and re-shipping; `dissolved` — split into halves (no
407
+ * validated member); #840, with VALIDATED members landed: `regate` — the
408
+ * rebase worked but the suite is red, so the gate re-runs over the landed
409
+ * members; `blocked` — no clean rebase, the landed members wait for an
410
+ * operator (`dissolve-refused:<reason>`). In both #840 outcomes only the
411
+ * unvalidated members were split off.
412
+ */
413
+ export type PrConflictAction = 'reship' | 'dissolved' | 'regate' | 'blocked';
361
414
  export interface PrConflictOutcome {
362
415
  state: SchedState;
363
416
  action: PrConflictAction;
@@ -372,8 +425,16 @@ export interface PrConflictOutcome {
372
425
  *
373
426
  * First occurrence: rebase the batch branch onto the base, re-run the suite,
374
427
  * and re-ship once. Second occurrence — or a rebase that conflicts, or a suite
375
- * that is red after a clean rebase — dissolves the batch into two half-batches:
376
- * the work is kept, the batch that could not land is not retried a third time.
428
+ * that is red after a clean rebase — gives up on re-shipping as-is:
429
+ *
430
+ * - no VALIDATED member → the batch dissolves into two half-batches: the work
431
+ * is kept, the batch that could not land is not retried a third time.
432
+ * - #840: VALIDATED members exist → they are never re-batched or re-dispatched
433
+ * (`keepLandedAndSplit`): they stay landed on the integration branch, and
434
+ * only the unvalidated members are split into half-batches. When the rebase
435
+ * onto the new base succeeded (red suite) the gate re-runs over the landed
436
+ * members (`validating` — the shape of #822's `sched resume --batch`);
437
+ * otherwise the batch blocks `dissolve-refused:<reason>` for an operator.
377
438
  */
378
439
  export declare function handlePrConflict(state: SchedState, batchId: string, deps: RecoveryDeps, opts?: {
379
440
  reason?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"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"}
package/dist/recovery.js CHANGED
@@ -37,6 +37,8 @@ exports.createExecMilestonePoster = createExecMilestonePoster;
37
37
  exports.beginAttribution = beginAttribution;
38
38
  exports.beginFixAttempt = beginFixAttempt;
39
39
  exports.resolveFixAttempt = resolveFixAttempt;
40
+ exports.listRemoteMemberBranches = listRemoteMemberBranches;
41
+ exports.pruneMemberBranches = pruneMemberBranches;
40
42
  exports.expandEvictionGroups = expandEvictionGroups;
41
43
  exports.evictMembers = evictMembers;
42
44
  exports.checkDissolveTrigger = checkDissolveTrigger;
@@ -302,6 +304,87 @@ function resolveFixAttempt(state, batchId, issue, outcome, deps) {
302
304
  }), now);
303
305
  return { state: next };
304
306
  }
307
+ /**
308
+ * #840: this batch's member branches that exist on ORIGIN right now —
309
+ * `batch/<batchId>-m<n>-<issue>` (`memberBranchFor`'s shape), matched exactly
310
+ * so another batch whose id merely starts with this one's (`b1-a`, a halved
311
+ * split) or the integration branch (`batch/<id>-<date>`) never matches.
312
+ * Since #840 a member branch outlives its landing — it is deleted when the
313
+ * batch ships or dissolves (`teardownBatch`), so this is also how an
314
+ * aggregate-suite eviction finds the branch a landed member can be parked on.
315
+ * `null` when origin could not be read (never "no branches").
316
+ */
317
+ function listRemoteMemberBranches(exec, repoDir, batchId) {
318
+ if (!attribution_1.SAFE_REF_RE.test(`batch/${batchId}`))
319
+ return null;
320
+ const out = exec('git', ['ls-remote', '--heads', 'origin', `refs/heads/batch/${batchId}-m*`], repoDir);
321
+ if (out === null)
322
+ return null;
323
+ const escaped = batchId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
324
+ const shape = new RegExp(`^refs/heads/(batch/${escaped}-m(\\d+)-(\\d+))$`);
325
+ const found = [];
326
+ for (const line of out.split('\n')) {
327
+ const ref = line.trim().split(/\s+/)[1];
328
+ const match = ref === undefined ? null : shape.exec(ref);
329
+ if (match === null)
330
+ continue;
331
+ found.push({
332
+ branch: match[1],
333
+ index: Number(match[2]),
334
+ issue: Number(match[3]),
335
+ });
336
+ }
337
+ return found;
338
+ }
339
+ /**
340
+ * #840: the live member branch `issue`'s work sits on — the recorded one
341
+ * (parallel run / current serial member) when origin still has it, else the
342
+ * highest-indexed live `batch/<id>-m<n>-<issue>`. `null` when none is live.
343
+ * When origin could not be listed (`live === null`) nothing can be verified:
344
+ * a RECORDED branch (a parallel run's, or the current serial member's) is
345
+ * still parked on — a `sched requeue` onto a branch that turns out to be gone
346
+ * simply starts fresh (`runstate verify` → setup) — and anything else falls
347
+ * back to the full-cycle requeue, journaled by the caller.
348
+ */
349
+ function liveMemberBranch(batch, issue, live) {
350
+ if (live === null)
351
+ return (0, state_1.recordedMemberBranch)(batch, issue);
352
+ const own = live.filter((b) => b.issue === issue);
353
+ const recorded = (0, state_1.recordedMemberBranch)(batch, issue);
354
+ if (recorded !== null && own.some((b) => b.branch === recorded))
355
+ return recorded;
356
+ return [...own].sort((a, b) => b.index - a.index)[0]?.branch ?? null;
357
+ }
358
+ /**
359
+ * #840: the batch is over — delete its member branches from origin. They
360
+ * outlive landing so an aggregate-suite eviction can park a landed member on
361
+ * its branch; once the batch ships or dissolves (`teardownBatch`), or is
362
+ * abandoned (`sched abandon --batch`), nothing of the batch needs them —
363
+ * except a branch a NON-terminal queue entry's `failure_evidence.branch`
364
+ * still names (a parked member, or one requeued to continue from it): that
365
+ * branch is the member's work and is kept. One `git push --delete` per ref,
366
+ * so one already-gone ref cannot mask the others' outcome. `null` when
367
+ * origin could not be listed (nothing deleted). Never throws.
368
+ */
369
+ function pruneMemberBranches(exec, repoDir, state, batchId) {
370
+ const live = listRemoteMemberBranches(exec, repoDir, batchId);
371
+ if (live === null)
372
+ return null;
373
+ const referenced = new Set(state.entries
374
+ .filter((e) => !types_1.TERMINAL_ISSUE_STATUSES.has(e.status))
375
+ .map((e) => e.failure_evidence?.branch)
376
+ .filter((b) => typeof b === 'string'));
377
+ const out = { deleted: [], kept: [], failed: [] };
378
+ for (const { branch } of live) {
379
+ if (referenced.has(branch)) {
380
+ out.kept.push(branch);
381
+ continue;
382
+ }
383
+ const ok = exec('git', ['push', 'origin', '--delete', branch], repoDir) !== null;
384
+ (ok ? out.deleted : out.failed).push(branch);
385
+ }
386
+ return out;
387
+ }
305
388
  /**
306
389
  * Every member that must leave with `issues`: an eviction group reverts
307
390
  * together (RFC-0001 §E.4), because a member built on another's API cannot
@@ -345,8 +428,9 @@ function orderRevertCommits(ranges, targets) {
345
428
  .map((commit) => commit.sha);
346
429
  }
347
430
  /**
348
- * `attributing → evicting`: revert the offending members' commits, requeue them
349
- * as full-cycle with their failure evidence, re-run the suite, and check the
431
+ * `attributing → evicting`: revert the offending members' commits, park each
432
+ * on its still-live member branch (#840; else requeue it as full-cycle) with
433
+ * its failure evidence, re-run the suite, and check the
350
434
  * batch's configurable dissolve threshold (`DissolvePolicy`, AC2/AC3/AC5).
351
435
  * Crossing it has two outcomes (#563): if the re-run suite came back green
352
436
  * for the survivors, the batch is PRESERVED — trimmed to its survivors and
@@ -378,6 +462,7 @@ function evictMembers(state, batchId, input, deps) {
378
462
  state: dissolve.state,
379
463
  evicted: [],
380
464
  requeued: dissolve.requeued,
465
+ parked: dissolve.parked,
381
466
  reverted: [],
382
467
  conflict: true,
383
468
  // #810: a refused dissolve (validated members kept) blocks instead.
@@ -432,7 +517,11 @@ function evictMembers(state, batchId, input, deps) {
432
517
  }
433
518
  record();
434
519
  }
435
- // Requeue every reverted member with its evidence attached (AC2).
520
+ // Requeue every reverted member with its evidence attached (AC2) — or,
521
+ // since #840, PARK it `evicted` on its member branch when that branch is
522
+ // still live on origin (a landed member's branch now outlives the landing):
523
+ // the reverted work is exactly what is on it, and an operator's `sched
524
+ // requeue` continues it there instead of restarting from the base.
436
525
  //
437
526
  // A member already in `evictions[]` is skipped entirely (#595 AC1): the
438
527
  // reverts above are idempotent (each checks the branch history first), but a
@@ -444,7 +533,15 @@ function evictMembers(state, batchId, input, deps) {
444
533
  // never silently dropped.
445
534
  const alreadyEvicted = new Map(batch.evictions.map((e) => [e.issue, e]));
446
535
  const requeued = [];
536
+ const parked = [];
447
537
  const records = [];
538
+ const needsBranch = targets.some((t) => !alreadyEvicted.has(t));
539
+ const live = needsBranch ? listRemoteMemberBranches(deps.exec, deps.repoDir, batchId) : [];
540
+ if (live === null) {
541
+ journal(deps, (0, journal_1.unitEvent)('git-failed', `batch:${batchId}`, {
542
+ detail: `git ls-remote origin batch/${batchId}-m* failed — member branches unverifiable: only a recorded member branch is parked on, other evicted members requeue full-cycle from the base`,
543
+ }), now);
544
+ }
448
545
  for (const issue of targets) {
449
546
  const prior = alreadyEvicted.get(issue);
450
547
  if (prior) {
@@ -464,18 +561,31 @@ function evictMembers(state, batchId, input, deps) {
464
561
  reverted_commits: memberCommits,
465
562
  at: now.toISOString(),
466
563
  };
467
- const result = (0, state_1.requeueMember)(next, issue, { mode: 'full', batch: null }, input.reason, now, {
468
- failure_evidence: evidence,
469
- });
470
- next = result.state;
471
- if (result.requeued)
472
- requeued.push(issue);
564
+ const branch = liveMemberBranch(batch, issue, live);
565
+ const park = branch === null
566
+ ? { state: next, parked: false }
567
+ : (0, state_1.parkMember)(next, issue, 'evicted', input.reason, now, {
568
+ failure_evidence: { ...evidence, branch },
569
+ });
570
+ next = park.state;
571
+ if (park.parked) {
572
+ parked.push(issue);
573
+ }
574
+ else {
575
+ const result = (0, state_1.requeueMember)(next, issue, { mode: 'full', batch: null }, input.reason, now, {
576
+ failure_evidence: evidence,
577
+ });
578
+ next = result.state;
579
+ if (result.requeued)
580
+ requeued.push(issue);
581
+ }
473
582
  records.push({
474
583
  issue,
475
584
  reason: input.reason,
476
585
  attribution: input.attribution,
477
586
  reverted_commits: memberCommits,
478
587
  group: targets.filter((t) => t !== issue),
588
+ ...(park.parked ? { kind: 'evicted', branch } : {}),
479
589
  at: now.toISOString(),
480
590
  });
481
591
  journal(deps, (0, journal_1.unitEvent)('member-evicted', `batch:${batchId}`, {
@@ -484,9 +594,10 @@ function evictMembers(state, batchId, input, deps) {
484
594
  // on the branch while the queue says it was evicted, so the batch would
485
595
  // ship code it believes it removed. `reverted=0` alone reads as "an
486
596
  // empty range", which is why this says it in words.
487
- detail: memberCommits.length === 0
597
+ detail: (memberCommits.length === 0
488
598
  ? `${input.reason} reverted=0 — NO commits found for this member on the batch branch; its work was NOT reverted`
489
- : `${input.reason} reverted=${memberCommits.length}`,
599
+ : `${input.reason} reverted=${memberCommits.length}`) +
600
+ (park.parked ? ` — parked on ${branch}` : ''),
490
601
  }), now);
491
602
  }
492
603
  // `appendEvictions` stays the state-level backstop; the loop above already
@@ -507,6 +618,7 @@ function evictMembers(state, batchId, input, deps) {
507
618
  reason: input.reason,
508
619
  evicted: targets.join(',') || 'none',
509
620
  requeued: requeued.join(',') || 'none',
621
+ ...(parked.length > 0 ? { parked: parked.join(',') } : {}),
510
622
  reverted: String(reverted.length),
511
623
  attribution: input.attribution,
512
624
  ...(withoutCommits.length > 0 ? { no_commits: withoutCommits.join(',') } : {}),
@@ -548,6 +660,7 @@ function evictMembers(state, batchId, input, deps) {
548
660
  // The surviving members the dissolve requeued belong here too — a caller
549
661
  // reading `requeued` must see everything that went back on the queue.
550
662
  requeued: [...new Set([...requeued, ...dissolve.requeued])].sort((a, b) => a - b),
663
+ parked: [...new Set([...parked, ...dissolve.parked])].sort((a, b) => a - b),
551
664
  reverted,
552
665
  conflict: false,
553
666
  dissolved,
@@ -559,6 +672,7 @@ function evictMembers(state, batchId, input, deps) {
559
672
  state: next,
560
673
  evicted: targets,
561
674
  requeued,
675
+ parked,
562
676
  reverted,
563
677
  conflict: false,
564
678
  dissolved: false,
@@ -582,6 +696,7 @@ function revertConflict(state, batchId, targets, deps, now, reverted, detail) {
582
696
  state: dissolve.state,
583
697
  evicted: [...targets],
584
698
  requeued: dissolve.requeued,
699
+ parked: dissolve.parked,
585
700
  reverted,
586
701
  conflict: true,
587
702
  // #810: a refused dissolve (validated members kept) blocks instead.
@@ -715,59 +830,10 @@ function dissolveBatch(state, batchId, opts, deps) {
715
830
  const requeued = [];
716
831
  const parked = [];
717
832
  if (strategy === 'halved' && unshipped.length > 0) {
718
- // Split by POSITION, not by coupling: the halves are the first and second
719
- // half of `unshipped` in member order. A coupled eviction group straddling
720
- // the pivot is therefore broken up — accepted, because both halves re-run
721
- // through the same validate/evict rails, where a member that cannot stand
722
- // alone is evicted rather than silently shipped.
723
- const pivot = Math.ceil(unshipped.length / 2);
724
- const taken = new Set(next.batches.map((b) => b.id));
725
- // A colliding id would produce two batches answering to one name:
726
- // `findBatch` would return the first and `validateState` would refuse to
727
- // load the file at all on the next start.
728
- const freeId = (base) => {
729
- let id = base;
730
- let n = 2;
731
- while (taken.has(id))
732
- id = `${base}${n++}`;
733
- taken.add(id);
734
- return id;
735
- };
736
- const halves = [
737
- { id: freeId(`${batchId}-a`), members: unshipped.slice(0, pivot) },
738
- { id: freeId(`${batchId}-b`), members: unshipped.slice(pivot) },
739
- ].filter((h) => h.members.length > 0);
740
- for (const half of halves) {
741
- next = {
742
- ...next,
743
- batches: [
744
- ...next.batches,
745
- (0, state_1.createBatch)(half.id, half.members, now, {
746
- base_branch: batch.base_branch,
747
- anchor: batch.anchor ?? undefined,
748
- run_id: batch.run_id ?? undefined,
749
- dispatch_profile: batch.dispatch_profile ?? undefined,
750
- // #565: a dissolve split is not a fresh batch — carry the
751
- // parent's priority forward, or an operator's `--priority`
752
- // (or `reprioritize`) is silently lost the moment a batch
753
- // dissolves into halves.
754
- priority: batch.priority,
755
- // Groups survive the split, restricted to the members that landed
756
- // in this half — a group spanning both halves is no longer a group.
757
- eviction_groups: batch.eviction_groups
758
- .map((group) => group.filter((m) => half.members.includes(m)))
759
- .filter((group) => group.length > 1),
760
- }),
761
- ],
762
- };
763
- newBatches.push(half.id);
764
- for (const issue of half.members) {
765
- const result = (0, state_1.requeueMember)(next, issue, { mode: 'slot', batch: half.id }, opts.reason, now, { failure_evidence: { ...evidence, batch: half.id } });
766
- next = result.state;
767
- if (result.requeued)
768
- requeued.push(issue);
769
- }
770
- }
833
+ const split = splitIntoHalves(next, batch, unshipped, opts.reason, evidence, now);
834
+ next = split.state;
835
+ newBatches.push(...split.newBatches);
836
+ requeued.push(...split.requeued);
771
837
  journal(deps, (0, journal_1.unitEvent)('batch-split', `batch:${batchId}`, { detail: newBatches.join(',') }), now);
772
838
  }
773
839
  else {
@@ -813,6 +879,76 @@ function dissolveBatch(state, batchId, opts, deps) {
813
879
  validated: [],
814
880
  };
815
881
  }
882
+ /**
883
+ * Split `members` of `batch` into two fresh `forming` half-batches and requeue
884
+ * each member into its half (§F.9: smaller batches, not abandoned work) — the
885
+ * `halved` dissolve's split, shared since #840 with `handlePrConflict`'s
886
+ * keep-landed path (which splits only the UNVALIDATED members).
887
+ *
888
+ * Split by POSITION, not by coupling: the halves are the first and second
889
+ * half of `members` in member order. A coupled eviction group straddling the
890
+ * pivot is therefore broken up — accepted, because both halves re-run
891
+ * through the same validate/evict rails, where a member that cannot stand
892
+ * alone is evicted rather than silently shipped.
893
+ */
894
+ function splitIntoHalves(state, batch, members, reason, evidence, now) {
895
+ let next = state;
896
+ const newBatches = [];
897
+ const requeued = [];
898
+ if (members.length === 0)
899
+ return { state: next, newBatches, requeued };
900
+ const pivot = Math.ceil(members.length / 2);
901
+ const taken = new Set(next.batches.map((b) => b.id));
902
+ // A colliding id would produce two batches answering to one name:
903
+ // `findBatch` would return the first and `validateState` would refuse to
904
+ // load the file at all on the next start.
905
+ const freeId = (base) => {
906
+ let id = base;
907
+ let n = 2;
908
+ while (taken.has(id))
909
+ id = `${base}${n++}`;
910
+ taken.add(id);
911
+ return id;
912
+ };
913
+ const halves = [
914
+ { id: freeId(`${batch.id}-a`), members: members.slice(0, pivot) },
915
+ { id: freeId(`${batch.id}-b`), members: members.slice(pivot) },
916
+ ].filter((h) => h.members.length > 0);
917
+ for (const half of halves) {
918
+ next = {
919
+ ...next,
920
+ batches: [
921
+ ...next.batches,
922
+ (0, state_1.createBatch)(half.id, half.members, now, {
923
+ base_branch: batch.base_branch,
924
+ anchor: batch.anchor ?? undefined,
925
+ run_id: batch.run_id ?? undefined,
926
+ dispatch_profile: batch.dispatch_profile ?? undefined,
927
+ // #565: a dissolve split is not a fresh batch — carry the
928
+ // parent's priority forward, or an operator's `--priority`
929
+ // (or `reprioritize`) is silently lost the moment a batch
930
+ // dissolves into halves.
931
+ priority: batch.priority,
932
+ // Groups survive the split, restricted to the members that landed
933
+ // in this half — a group spanning both halves is no longer a group.
934
+ eviction_groups: batch.eviction_groups
935
+ .map((group) => group.filter((m) => half.members.includes(m)))
936
+ .filter((group) => group.length > 1),
937
+ }),
938
+ ],
939
+ };
940
+ newBatches.push(half.id);
941
+ for (const issue of half.members) {
942
+ const result = (0, state_1.requeueMember)(next, issue, { mode: 'slot', batch: half.id }, reason, now, {
943
+ failure_evidence: { ...evidence, batch: half.id },
944
+ });
945
+ next = result.state;
946
+ if (result.requeued)
947
+ requeued.push(issue);
948
+ }
949
+ }
950
+ return { state: next, newBatches, requeued };
951
+ }
816
952
  /**
817
953
  * #810: sort a dissolving batch's members. `preserved` = no longer the
818
954
  * batch's to decide (shipped/terminal, redispatched elsewhere, or already
@@ -1050,8 +1186,16 @@ function blockBatch(state, batchId, opts, deps) {
1050
1186
  *
1051
1187
  * First occurrence: rebase the batch branch onto the base, re-run the suite,
1052
1188
  * and re-ship once. Second occurrence — or a rebase that conflicts, or a suite
1053
- * that is red after a clean rebase — dissolves the batch into two half-batches:
1054
- * the work is kept, the batch that could not land is not retried a third time.
1189
+ * that is red after a clean rebase — gives up on re-shipping as-is:
1190
+ *
1191
+ * - no VALIDATED member → the batch dissolves into two half-batches: the work
1192
+ * is kept, the batch that could not land is not retried a third time.
1193
+ * - #840: VALIDATED members exist → they are never re-batched or re-dispatched
1194
+ * (`keepLandedAndSplit`): they stay landed on the integration branch, and
1195
+ * only the unvalidated members are split into half-batches. When the rebase
1196
+ * onto the new base succeeded (red suite) the gate re-runs over the landed
1197
+ * members (`validating` — the shape of #822's `sched resume --batch`);
1198
+ * otherwise the batch blocks `dissolve-refused:<reason>` for an operator.
1055
1199
  */
1056
1200
  function handlePrConflict(state, batchId, deps, opts = {}) {
1057
1201
  const now = clock(deps);
@@ -1065,8 +1209,20 @@ function handlePrConflict(state, batchId, deps, opts = {}) {
1065
1209
  if (batch.status !== 'rebasing') {
1066
1210
  next = (0, state_1.transitionBatch)(next, batchId, 'rebasing', {}, now);
1067
1211
  }
1068
- /** Every give-up path here ends the same way: halve the batch, keep the work. */
1212
+ /**
1213
+ * Every give-up path here ends the same way: keep the work. Validated
1214
+ * members stay landed (#840); everything else is halved.
1215
+ */
1069
1216
  const bailToHalves = (from, bailReason, rebased = false, suite = null) => {
1217
+ const { validated } = classifyDissolveMembers(from, batchOrThrow(from, batchId));
1218
+ if (validated.length > 0) {
1219
+ return keepLandedAndSplit(from, batchId, deps, {
1220
+ reason: bailReason,
1221
+ validated,
1222
+ rebased,
1223
+ suite,
1224
+ });
1225
+ }
1070
1226
  const dissolve = dissolveBatch(from, batchId, { strategy: 'halved', reason: bailReason, milestonePhase: 'batch-ship' }, deps);
1071
1227
  return { state: dissolve.state, action: 'dissolved', rebased, suite, dissolve };
1072
1228
  };
@@ -1126,4 +1282,98 @@ function handlePrConflict(state, batchId, deps, opts = {}) {
1126
1282
  }, now);
1127
1283
  return { state: next, action: 'reship', rebased: true, suite };
1128
1284
  }
1285
+ /**
1286
+ * #840: `handlePrConflict`'s give-up path when VALIDATED members exist. They
1287
+ * are never re-batched or re-dispatched: they stay in this batch, landed on
1288
+ * its integration branch. Only the unshipped, unvalidated members leave — split
1289
+ * into half-batches exactly as a `halved` dissolve would, and trimmed out of
1290
+ * this batch (with their groups/ranges, like `preserveSurvivors`). Then:
1291
+ *
1292
+ * - `rebased` (the rebase onto the new base worked, the suite is red) →
1293
+ * `re-validating → validating` with `executing_member` pinned to the end,
1294
+ * so the engine re-runs the gate over the landed members and attributes /
1295
+ * evicts on the ordinary rail — the same shape as `sched resume --batch`.
1296
+ * - otherwise (rebase conflict, failed fetch, a recurred conflict, …) → the
1297
+ * batch BLOCKS `dissolve-refused:<reason>`: the landed work waits for an
1298
+ * operator — resolve the conflict on the recorded PR by hand
1299
+ * (`reconcileStaleBlockedBatches` settles the batch once it merges), or
1300
+ * close that PR and `sched resume --batch` (which refuses while a PR is
1301
+ * recorded), or `sched abandon --batch`.
1302
+ *
1303
+ * Library behaviour: the engine does not yet route a conflicting batch PR
1304
+ * here (#867).
1305
+ */
1306
+ function keepLandedAndSplit(state, batchId, deps, ctx) {
1307
+ const now = clock(deps);
1308
+ const batch = batchOrThrow(state, batchId);
1309
+ const { unshipped } = classifyDissolveMembers(state, batch);
1310
+ // A member with a recorded range has commits ON the integration branch this
1311
+ // batch keeps — splitting it off would ship its code here AND re-run it in
1312
+ // a half. It stays (not validated, so the tail never names it; the re-run
1313
+ // gate or the operator decides it). Only unlanded, unvalidated members go.
1314
+ const landed = new Set(batch.ranges.map((r) => r.issue));
1315
+ const toSplit = unshipped.filter((issue) => !ctx.validated.includes(issue) && !landed.has(issue));
1316
+ const evidence = (0, state_1.memberExitEvidence)(batchId, ctx.reason, null, now);
1317
+ const split = splitIntoHalves(state, batch, toSplit, ctx.reason, evidence, now);
1318
+ let next = split.state;
1319
+ if (split.newBatches.length > 0) {
1320
+ journal(deps, (0, journal_1.unitEvent)('batch-split', `batch:${batchId}`, {
1321
+ detail: `${split.newBatches.join(',')} — unvalidated member(s) ${toSplit.join(',')} only; validated ${ctx.validated.join(',')} stay landed`,
1322
+ }), now);
1323
+ }
1324
+ const kept = batch.members.filter((issue) => !toSplit.includes(issue));
1325
+ next = (0, state_1.patchBatch)(next, batchId, {
1326
+ members: kept,
1327
+ eviction_groups: batch.eviction_groups
1328
+ .map((group) => group.filter((m) => kept.includes(m)))
1329
+ .filter((group) => group.length > 1),
1330
+ ranges: batch.ranges.filter((r) => kept.includes(r.issue)),
1331
+ executing_member: kept.length,
1332
+ }, now);
1333
+ const dissolve = {
1334
+ state: next,
1335
+ requeued: split.requeued,
1336
+ preserved: ctx.validated,
1337
+ newBatches: split.newBatches,
1338
+ parked: [],
1339
+ blocked: !ctx.rebased,
1340
+ validated: ctx.validated,
1341
+ };
1342
+ if (ctx.rebased) {
1343
+ next = (0, state_1.transitionBatch)(next, batchId, 'validating', {}, now);
1344
+ journal(deps, (0, journal_1.unitEvent)('batch-regate', `batch:${batchId}`, {
1345
+ detail: `${ctx.reason} — gate re-runs over landed member(s) ${ctx.validated.join(',')} on the rebased branch; split=${split.newBatches.join(',') || 'none'}`,
1346
+ }), now);
1347
+ return {
1348
+ state: next,
1349
+ action: 'regate',
1350
+ rebased: true,
1351
+ suite: ctx.suite,
1352
+ dissolve: { ...dissolve, state: next },
1353
+ };
1354
+ }
1355
+ const blockedReason = `${exports.DISSOLVE_REFUSED_PREFIX}${ctx.reason}`;
1356
+ next = (0, state_1.transitionBatch)(next, batchId, 'blocked', { blocked_reason: blockedReason }, now);
1357
+ journal(deps, (0, journal_1.unitEvent)('batch-blocked', `batch:${batchId}`, {
1358
+ detail: `${blockedReason} — validated member(s) ${ctx.validated.join(',')} stay landed on ${batch.branch ?? 'the integration branch'}; split=${split.newBatches.join(',') || 'none'}`,
1359
+ }), now);
1360
+ post(deps, batchOrThrow(next, batchId), {
1361
+ phase: 'batch-ship',
1362
+ status: 'blocked',
1363
+ kv: {
1364
+ reason: blockedReason,
1365
+ dissolved: 'false',
1366
+ validated: ctx.validated.join(','),
1367
+ requeued: split.requeued.join(',') || 'none',
1368
+ ...(split.newBatches.length > 0 ? { split_into: split.newBatches.join(',') } : {}),
1369
+ },
1370
+ }, now);
1371
+ return {
1372
+ state: next,
1373
+ action: 'blocked',
1374
+ rebased: false,
1375
+ suite: ctx.suite,
1376
+ dissolve: { ...dissolve, state: next },
1377
+ };
1378
+ }
1129
1379
  //# sourceMappingURL=recovery.js.map