Charter comparison across hosts: fleet_list drops charterBytes, and a digest cannot tell you what changed #604

Open
opened 2026-09-20 11:08:29 +02:00 by ltms · 0 comments
Owner

Context

charterSha256 in fleet_list turned out to be more useful than expected. Because the profile is not part of the digest, two fleets on two hosts can compare the digest directly and see whether their members got the same charter. The first time it was used that way it found our architect charters had silently drifted apart by 514 bytes.

This ticket records three limits I hit while using it. The first is a concrete gap in the code. The other two are limits of the method, and they belong somewhere a future session will read before trusting a comparison.

1. fleet_list reports the digest but drops the size

CharterReceipt carries both:

public record CharterReceipt(
        MemberRole role,
        String profile,
        String charterSource,
        String charterSha256,
        int charterBytes) {

The spawn log prints both — HerdrPeerLauncher.java:549:

log.info("spawned role={} profile={} charterSource={} charterSha256={} charterBytes={}",

But the roster projection keeps only the digest — SessionManager.java:874:

if (session.charterReceipt() != null) {
    m.put("charterSource", session.charterReceipt().charterSource());
    if (session.charterReceipt().charterSha256() != null) {
        m.put("charterSha256", session.charterReceipt().charterSha256());
    }
}

charterBytes is never put in the map.

Why the size matters even though the digest is stronger. A digest answers one question: same or different. When two hosts differ, that is the moment you most need a second, weaker signal, because the digest cannot tell you how they differ. A byte count turns "they differ" into "they differ by 514 bytes, so a paragraph was added, not a word changed" — and that is the difference between guessing and knowing where to look.

It costs one line, it is already measured, and it leaks nothing: a length is not content. The javadoc's own reasoning — "never the charter text itself", the source "carries no content" — applies to the size too.

Note the digest is written conditionally, so a member with no configured charter reports charterSource and no digest. Whatever is done with charterBytes should be consistent with that, and the test for it should prove the no-charter case as well as the normal one. A test that only covers a member with a charter can be passed by code that always writes the field.

2. A digest over paragraphs is prose only — list structure does not survive

While comparing two charters I reduced each to a paragraph count and a per-paragraph byte list, then digested that. It works for prose. It quietly does not work for anything structured.

A bullet list, an indented block, or a table is not a paragraph. Depending on how the text is split, a list either collapses into one long paragraph or explodes into one paragraph per item. Two charters that render identically can produce different paragraph lists, and two that differ in list structure can produce the same one.

So: a paragraph-level comparison is evidence about prose charters and nothing else. If a charter ever grows a list, the comparison must be re-derived, not re-run.

3. The digest authenticates an artifact, not a derivation

This is the one that can mislead a future session most.

charterSha256 is a digest of the charter the daemon actually composed and handed to the member. That is an artifact. It is trustworthy.

A digest I compute by hand over a paragraph list is a digest of my derivation of that text. If my split is wrong, or I normalised whitespace differently on two hosts, the two digests differ while the underlying charters are identical — or match while they are not. The digest still looks authoritative, because a sha256 always does.

The rule that follows: only compare digests produced by the same instrument. A charterSha256 from fleet_list on one host may be compared with charterSha256 from fleet_list on another. A hand-computed digest may only be compared with another hand-computed digest from the same script. Mixing the two is not a comparison, and it will read as a clean result either way.

This is the same shape as: N agreeing measurements are one data point when they share an instrument. Two hosts, two operators and one formula is one formula, not two confirmations.

Suggested split

Item 1 is a small code change with a test, and can be done on its own.

Items 2 and 3 are not code. They belong in the javadoc of CharterReceipt, next to the existing note about why the text is never included — that is where someone will be standing when they decide to trust a digest.

Not done

I have not fixed any of these. Item 1 I found by reading the code just now; items 2 and 3 I hit while using the digest to compare two live charters.

## Context `charterSha256` in `fleet_list` turned out to be more useful than expected. Because the profile is not part of the digest, two fleets on two hosts can compare the digest directly and see whether their members got the same charter. The first time it was used that way it found our architect charters had silently drifted apart by 514 bytes. This ticket records three limits I hit while using it. The first is a concrete gap in the code. The other two are limits of the method, and they belong somewhere a future session will read before trusting a comparison. ## 1. `fleet_list` reports the digest but drops the size `CharterReceipt` carries both: ```java public record CharterReceipt( MemberRole role, String profile, String charterSource, String charterSha256, int charterBytes) { ``` The spawn log prints both — `HerdrPeerLauncher.java:549`: ```java log.info("spawned role={} profile={} charterSource={} charterSha256={} charterBytes={}", ``` But the roster projection keeps only the digest — `SessionManager.java:874`: ```java if (session.charterReceipt() != null) { m.put("charterSource", session.charterReceipt().charterSource()); if (session.charterReceipt().charterSha256() != null) { m.put("charterSha256", session.charterReceipt().charterSha256()); } } ``` `charterBytes` is never put in the map. **Why the size matters even though the digest is stronger.** A digest answers one question: same or different. When two hosts differ, that is the moment you most need a second, weaker signal, because the digest cannot tell you *how* they differ. A byte count turns "they differ" into "they differ by 514 bytes, so a paragraph was added, not a word changed" — and that is the difference between guessing and knowing where to look. It costs one line, it is already measured, and it leaks nothing: a length is not content. The javadoc's own reasoning — "never the charter text itself", the source "carries no content" — applies to the size too. Note the digest is written conditionally, so a member with no configured charter reports `charterSource` and no digest. Whatever is done with `charterBytes` should be consistent with that, and the test for it should prove the no-charter case as well as the normal one. A test that only covers a member with a charter can be passed by code that always writes the field. ## 2. A digest over paragraphs is prose only — list structure does not survive While comparing two charters I reduced each to a paragraph count and a per-paragraph byte list, then digested that. It works for prose. It quietly does not work for anything structured. A bullet list, an indented block, or a table is not a paragraph. Depending on how the text is split, a list either collapses into one long paragraph or explodes into one paragraph per item. Two charters that render identically can produce different paragraph lists, and two that differ in list structure can produce the same one. So: a paragraph-level comparison is evidence about prose charters and nothing else. If a charter ever grows a list, the comparison must be re-derived, not re-run. ## 3. The digest authenticates an artifact, not a derivation This is the one that can mislead a future session most. `charterSha256` is a digest of the charter the daemon actually composed and handed to the member. That is an artifact. It is trustworthy. A digest I compute by hand over a paragraph list is a digest of **my derivation** of that text. If my split is wrong, or I normalised whitespace differently on two hosts, the two digests differ while the underlying charters are identical — or match while they are not. The digest still looks authoritative, because a sha256 always does. The rule that follows: only compare digests produced by the same instrument. A `charterSha256` from `fleet_list` on one host may be compared with `charterSha256` from `fleet_list` on another. A hand-computed digest may only be compared with another hand-computed digest from the same script. Mixing the two is not a comparison, and it will read as a clean result either way. This is the same shape as: N agreeing measurements are one data point when they share an instrument. Two hosts, two operators and one formula is one formula, not two confirmations. ## Suggested split Item 1 is a small code change with a test, and can be done on its own. Items 2 and 3 are not code. They belong in the javadoc of `CharterReceipt`, next to the existing note about why the text is never included — that is where someone will be standing when they decide to trust a digest. ## Not done I have not fixed any of these. Item 1 I found by reading the code just now; items 2 and 3 I hit while using the digest to compare two live charters.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#604