Correct the invariant claimed in the charterBytes comment
CI / shell-tests (pull_request) Failing after 7s
CI / build (pull_request) Successful in 1m36s
CI / contract (pull_request) Successful in 1m42s

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.
This commit is contained in:
Dai Ha
2026-09-20 16:16:30 +07:00
parent 8b986a52e0
commit ad3d81941f
@@ -872,11 +872,17 @@ public final class SessionManager implements TurnListener {
// records whether a role charter was configured ("fleet.charters.<role>") or only the reply // records whether a role charter was configured ("fleet.charters.<role>") or only the reply
// charter was composed ("none"). // charter was composed ("none").
// #604: charterBytes rides along with charterSha256, not with charterSource — it is only // #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"), // 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 // A member with no composed charter reports charterSource and nothing else, as before.
// 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 // Gating on the DIGEST rather than on the receipt is deliberate, and the two are not
// nothing else, exactly as before this change. // 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.<role> 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) { if (session.charterReceipt() != null) {
m.put("charterSource", session.charterReceipt().charterSource()); m.put("charterSource", session.charterReceipt().charterSource());
if (session.charterReceipt().charterSha256() != null) { if (session.charterReceipt().charterSha256() != null) {