From ad3d81941f46f3046de95136e043f7bfc2d16a25 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sun, 20 Sep 2026 16:16:30 +0700 Subject: [PATCH] Correct the invariant claimed in the charterBytes comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment said CharterReceipt never pairs a null digest with a non-zero byte count. It can. compose() derives the digest with digestOf(), which returns null for blank text, while the byte count is getBytes().length, which does not. A whitespace-only role charter on a profile with no MCP produces exactly that pair. No behaviour change. The gate already omits both fields on that path, which is the right answer — a size with no digest would describe an artifact we cannot fingerprint. Only the stated reason was wrong, and a false invariant in a comment is worse than no comment, because the next reader will widen the gate on the strength of it. --- .../dev/ltms/fleet/session/SessionManager.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/fleetd/src/main/java/dev/ltms/fleet/session/SessionManager.java b/fleetd/src/main/java/dev/ltms/fleet/session/SessionManager.java index af23289..ae06834 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/session/SessionManager.java +++ b/fleetd/src/main/java/dev/ltms/fleet/session/SessionManager.java @@ -872,11 +872,17 @@ public final class SessionManager implements TurnListener { // records whether a role charter was configured ("fleet.charters.") or only the reply // charter was composed ("none"). // #604: charterBytes rides along with charterSha256, not with charterSource — it is only - // meaningful as the digest's companion (a length turns "they differ" into "by how much"), - // and the receipt only ever pairs a non-null digest with a real byte count (CharterReceipt's - // own contract: no composed charter is an explicit null digest AND a zero byte count, never - // one without the other). A member with no composed charter at all reports charterSource and - // nothing else, exactly as before this change. + // meaningful as the digest's companion (a length turns "they differ" into "by how much"). + // A member with no composed charter reports charterSource and nothing else, as before. + // + // Gating on the DIGEST rather than on the receipt is deliberate, and the two are not + // always null together. CharterReceipt.compose() derives the digest with digestOf(), which + // returns null for BLANK text, while the byte count is composed.getBytes().length, which + // does not. So a whitespace-only charter (a blank fleet.charters. on a profile with + // no MCP, so no reply charter is appended) yields a null digest beside a non-zero size. + // Reporting a size with no digest would say "they differ by N bytes" about an artifact we + // cannot fingerprint, so this gate omits both. Never widen it to the receipt-level null + // check without deciding what that case should report. if (session.charterReceipt() != null) { m.put("charterSource", session.charterReceipt().charterSource()); if (session.charterReceipt().charterSha256() != null) {