Raised as a non-blocking note during the #1161 gate (pod 57632) and shipped unaddressed, which is fine — it was non-blocking — but it now lives only in a merged PR's pod message, so filing it where it survives.
Measured on origin/main at cde4d8e5, 05:35:50Z.
The behaviour
Message.reRootOrphanedChains (backend/models/pg/Message.ts:346) converges by breaking when a pass finds nothing:
static async reRootOrphanedChains(maxPasses = 32): Promise<{ reRooted: number; passes: number }> {
for (let i = 0; i < maxPasses; i += 1) {
// ... SELECT one level, UPDATE by id ...
const n = rows.length;
passes += 1;
reRooted += n;
if (n === 0) break;
}
return { reRooted, passes };
}
If the loop instead exits by exhausting maxPasses, un-rooted rows remain and the return value is indistinguishable from a completed repair. The caller (deleteOlderThan, :412-419) logs:
[pg-retention] re-rooted N orphaned reply row(s) in 32 pass(es) after deleting D
which reads as success. A caller cannot detect the difference either: { reRooted, passes } carries no flag, and passes === maxPasses is ambiguous — a chain of exactly depth 32 converges in 32 passes and breaks legitimately.
Reachability
The doc comment at :331 states the convergence property: "depth d converges in d passes." So the threshold is a reply chain deeper than 32. Unlikely in a chat pod, and the deeper the chain the more the repair matters — which is the wrong way round for a silent cap.
Why it is worth a line
The failure direction is safe: unrepaired rows render expanded, which is noisy and non-destructive — the same argument that justifies the catch in deleteOlderThan. So this is not urgent. But the whole point of that function is to leave no orphans behind, and right now a partial repair reports as a total one, on the only surface anybody watches.
Suggested fix
Distinguish the two exits, e.g. return { reRooted, passes, converged } where converged is set by the break, and have the caller say so when it is false. One line in each place.
Not verified
I have not constructed a >32-deep chain to observe the truncation. This is read from the loop's structure, not reproduced. The live orphan count is currently 0 (measured 04:23Z, instance-wide, edges=250 rooted=250), so nothing is being truncated today.
Context: #1161 (TASK-043) merged at 7f677235, 05:18:01Z. Tests at retentionReRoot.test.js cover convergence (:102, "a deeper chain converges, one level per pass") but not exhaustion.
Raised as a non-blocking note during the #1161 gate (pod 57632) and shipped unaddressed, which is fine — it was non-blocking — but it now lives only in a merged PR's pod message, so filing it where it survives.
Measured on
origin/mainatcde4d8e5, 05:35:50Z.The behaviour
Message.reRootOrphanedChains(backend/models/pg/Message.ts:346) converges by breaking when a pass finds nothing:If the loop instead exits by exhausting
maxPasses, un-rooted rows remain and the return value is indistinguishable from a completed repair. The caller (deleteOlderThan,:412-419) logs:which reads as success. A caller cannot detect the difference either:
{ reRooted, passes }carries no flag, andpasses === maxPassesis ambiguous — a chain of exactly depth 32 converges in 32 passes and breaks legitimately.Reachability
The doc comment at
:331states the convergence property: "depth d converges in d passes." So the threshold is a reply chain deeper than 32. Unlikely in a chat pod, and the deeper the chain the more the repair matters — which is the wrong way round for a silent cap.Why it is worth a line
The failure direction is safe: unrepaired rows render expanded, which is noisy and non-destructive — the same argument that justifies the
catchindeleteOlderThan. So this is not urgent. But the whole point of that function is to leave no orphans behind, and right now a partial repair reports as a total one, on the only surface anybody watches.Suggested fix
Distinguish the two exits, e.g. return
{ reRooted, passes, converged }whereconvergedis set by thebreak, and have the caller say so when it is false. One line in each place.Not verified
I have not constructed a >32-deep chain to observe the truncation. This is read from the loop's structure, not reproduced. The live orphan count is currently 0 (measured 04:23Z, instance-wide,
edges=250 rooted=250), so nothing is being truncated today.Context: #1161 (TASK-043) merged at
7f677235, 05:18:01Z. Tests atretentionReRoot.test.jscover convergence (:102, "a deeper chain converges, one level per pass") but not exhaustion.