commit 4a519d575571eb5b134b2689bd9c895e660130a0
parent 5cef97da786ba04872c24c9e8f13713231df0e3f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 17 Sep 2026 14:07:30 -0400
relocate: name the early stop in the summary, and the retry in the done line
Review fixes for S1.
The auto runner comment was wrong about its own cost: the `finally` only calls
markCompleted on a BUCKET lane, so an operation lane retires nothing. It says
so now, and says what actually defends the channel — GUARD 2 in
buildChannelWork drops an in-transition channel from the next tick, which
leaves this guard covering the pick-to-run window alone.
An early stop that only appears mid-log reads as a quiet, complete run in the
summary line, which is the line an operator actually reads.
`stoppedForRelocation` rides beside `spendCapped` and both lane summaries
append it, the same way the cap is consumed.
And the verify retry now reaches the operator too: the relocation job says a
directory timestamp was settled by a second pass, because the person reading
that line has watched this exact drift refuse a 131 GB move.
Plus two tidies: one copy of the verify paragraph rather than two, and `||=`
in moveBack to match moveOut.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
6 files changed, 47 insertions(+), 19 deletions(-)
diff --git a/common/controller/autoRunner.ts b/common/controller/autoRunner.ts
@@ -1656,12 +1656,20 @@ async function runLoop(
// left the verify refusing a 131 GB transfer. A marker is the signal, the
// same one every other guard already honours.
//
- // TRADE-OFF, ACCEPTED: the `finally` below marks the video completed for
- // the session, so this unit is not offered again until the runner
- // restarts or the snapshot regenerates. The window is one unit per
- // channel per session, and the relocation finishing re-enables the
- // channel on the next sweep — which is a far cheaper loss than a copy
- // refused at its last step.
+ // THIS IS THE BACKSTOP, NOT THE DEFENCE. GUARD 2 in buildChannelWork
+ // already drops the channel from the NEXT tick's pending list — any
+ // marker makes inspectChannelMedia report `in-transition`, which is
+ // neither `ok` nor `in-place` — so the only window this closes is
+ // pick→run, and it is measured in milliseconds.
+ //
+ // WHAT THE `finally` DOES WITH IT DIFFERS BY LANE, and neither answer
+ // costs anything. On a BUCKET lane it calls markCompleted, so this video
+ // is retired for the session and not offered again until the runner
+ // restarts or the snapshot regenerates: one unit per channel, accepted,
+ // and far cheaper than a copy refused at its last step. On an OPERATION
+ // lane it retires nothing — runOperationPick owns the (operation, video)
+ // keys and never ran — which is equally fine, because guard 2 has taken
+ // the channel off the list before the next tick could offer it again.
const marker = await readRelocationMarker(paths, channelSlug);
if (marker) {
onLog(
diff --git a/common/controller/operationBatch.ts b/common/controller/operationBatch.ts
@@ -1517,6 +1517,10 @@ export type OperationBatchResult = {
warnings: number;
// True when the run stopped early because the spend cap was reached.
spendCapped: boolean;
+ // True when the run ended early because a relocation of this channel's media
+ // started while it was running. Not a failure: the work that is left is still
+ // there, and the next sweep picks it up once the move is done.
+ stoppedForRelocation: boolean;
};
export function emptyOperationBatchResult(): OperationBatchResult {
@@ -1540,6 +1544,7 @@ export function emptyOperationBatchResult(): OperationBatchResult {
costUsd: 0,
warnings: 0,
spendCapped: false,
+ stoppedForRelocation: false,
};
}
@@ -1722,6 +1727,7 @@ export async function runOperationBatch(
`to ${marker.target} is in flight — the rest of this batch is left ` +
`for the next pass.`,
);
+ result.stoppedForRelocation = true;
return null;
}
// RE-DERIVED FROM DISK, every pull. A restart, a concurrent lane, or a
diff --git a/common/controller/operationBatchRelocation.test.ts b/common/controller/operationBatchRelocation.test.ts
@@ -86,6 +86,7 @@ test("every candidate is classified when no relocation is in flight", async () =
// The control for the case below: both videos are reached and counted.
assert.equal(result.missingInput, 2);
assert.equal(result.attempted, 0);
+ assert.equal(result.stoppedForRelocation, false);
assert.ok(!lines.some((l) => l.includes("Stopping: a relocation")));
});
@@ -126,6 +127,9 @@ test("a marker written mid-run ends the batch instead of racing the copy", async
// than working its way through the rest of the channel while rsync copied it.
assert.equal(result.missingInput, 1);
assert.equal(result.attempted, 0);
+ // The flag is how the job's summary line says why it stopped — an early end
+ // that the counters alone would render as a quiet, complete run.
+ assert.equal(result.stoppedForRelocation, true);
const stop = lines.find((l) => l.includes("Stopping: a relocation"));
assert.ok(stop, `expected a stop line, got:\n${lines.join("\n")}`);
assert.match(stop as string, /\(out\) of omni to \/platter\/omni\/data/);
diff --git a/common/controller/operationJobs.ts b/common/controller/operationJobs.ts
@@ -144,6 +144,9 @@ export async function runBackfillChannelJob(
: "")
: "") +
(batch.diskFloorHit ? " (stopped re-acquiring at the disk floor)" : "") +
+ (batch.stoppedForRelocation
+ ? " (stopped: a relocation is in flight)"
+ : "") +
".",
);
requestChannelSnapshot(paths, channelSlug);
@@ -266,6 +269,9 @@ export async function runDigestChannelJob(
`${batch.engineCalls} model call(s), ${batch.warnings} warning(s)` +
(batch.costUsd > 0 ? `, $${batch.costUsd.toFixed(4)}` : "") +
(batch.spendCapped ? " (stopped at the spend cap)" : "") +
+ (batch.stoppedForRelocation
+ ? " (stopped: a relocation is in flight)"
+ : "") +
".",
);
// ONCE, at job end — the digest kinds are in NO_REGEN_KINDS precisely so
diff --git a/common/controller/relocateChannelMedia.ts b/common/controller/relocateChannelMedia.ts
@@ -61,7 +61,9 @@ export type RelocateChannelMediaResult = {
// starting from scratch.
resumed: boolean;
// True when the verify found ONLY directory-mtime drift and one extra
- // `rsync -a` pass settled it. Content drift is still a refusal, never this.
+ // `rsync -a` pass settled it — worth saying on the job's done line, because
+ // an operator who has seen this refuse a copy should be told it did not this
+ // time. Content drift is still a refusal, never this.
retried: boolean;
};
@@ -333,10 +335,6 @@ async function rsyncTree(opts: {
return { exitCode: result.exitCode ?? 1, output };
}
-// What a copy has to clear before the swap: rsync itself agrees there is
-// nothing left to send, AND the two trees measure the same. The dry run alone
-// would accept a target that is byte-identical for the wrong reason; the counts
-// alone would accept two trees of equal size with different contents.
// A DIRECTORY MTIME IS NOT CONTENT. `.d..t` is rsync's itemization for "this is
// a directory and only its modification time differs" — nothing to send, and no
// byte of the copy is in question. It is what the omnimirror move hit
@@ -682,9 +680,9 @@ async function moveOut(args: {
if (exitCode !== 0) throw new Error(`rsync failed (exit ${exitCode})`);
log("Verifying the copy…");
- verifyRetried =
- (await verifyCopy({ paths, src: dataDir, dest: target, log, signal }))
- .retried || verifyRetried;
+ verifyRetried ||= (
+ await verifyCopy({ paths, src: dataDir, dest: target, log, signal })
+ ).retried;
phase = "swap";
}
@@ -712,9 +710,9 @@ async function moveOut(args: {
state.kind === "real-dir" ? dataDir : (parked[0] ?? null);
if (verifySrc) {
log("Verifying the copy…");
- verifyRetried =
- (await verifyCopy({ paths, src: verifySrc, dest: target, log, signal }))
- .retried || verifyRetried;
+ verifyRetried ||= (
+ await verifyCopy({ paths, src: verifySrc, dest: target, log, signal })
+ ).retried;
}
if (state.kind === "real-dir") {
@@ -895,7 +893,7 @@ async function moveBack(args: {
}
if (exitCode !== 0) throw new Error(`rsync failed (exit ${exitCode})`);
log("Verifying the copy…");
- verifyRetried = (
+ verifyRetried ||= (
await verifyCopy({ paths, src: target, dest: incoming, log, signal })
).retried;
phase = "swap";
diff --git a/editor/app/channels/lib/relocationJob.ts b/editor/app/channels/lib/relocationJob.ts
@@ -56,7 +56,13 @@ export async function enqueueRelocation(opts: {
onLog(
`${direction === "out" ? "Moved" : "Moved back"} ${result.files} file(s) / ` +
`${formatBytes(result.bytes)} — ${result.target}` +
- (result.resumed ? " (resumed an interrupted move)" : ""),
+ (result.resumed ? " (resumed an interrupted move)" : "") +
+ // Said out loud because the operator has watched this refuse a move:
+ // a directory timestamp left by a sidecar written mid-copy used to
+ // fail the verify with 131 GB correctly on the far side.
+ (result.retried
+ ? " (one directory timestamp settled by a second pass)"
+ : ""),
);
// No snapshot regen — deliberately, and `relocate-channel-media` is in
// NO_REGEN_KINDS so the central hook does not arm one either. The move