Skip to content

feat(commands): carry payloads, state, results, and effects as Bytes blobs - #508

Merged
paymog merged 1 commit into
mainfrom
feat/commands-bytes-504
Oct 19, 2026
Merged

paymog merged 1 commit into
mainfrom
feat/commands-bytes-504

Conversation

@paymog

@paymog paymog commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Summary

commands now carries payloads, state, results, and effects as Bytes.Bytes, stored as SQLite BLOBs. A webhook body goes in as it arrives, with no conversion to a byte String, and a payload with a NUL byte now round-trips and deduplicates. SQLite's text binding rejected NUL before.

This is a breaking change: commands goes to 0.2.0.0. A 0.1 database stores TEXT where 0.2 reads BLOBs, so 0.2 needs a new file. The README says so.

Design decision: affine step, not a Data stand-in

Bytes is affine (Type), so it cannot sit in the old Data records. The issue offered two choices. I took a third: step and its types are now generic over Type. The laws keep their full strength over the real payload. A digest key would have proven facts about the digest only.

An affine step cannot use a value twice, so two things changed:

  • step takes ~dup_p and ~dup_r. A new command's payload goes to decide and to the record. Its result goes to the record and to the reply. handle passes dup (Array.clone). Copies happen only for new commands, not duplicates.
  • A refusal now answers Note{record, reply} and writes no state row. Before, it answered Write with the old state. decide consumes the state, and rewriting an unchanged row did nothing anyway.

Law changes (LAWS.bend)

  • All five laws now quantify over Type and take ~dup_p and ~dup_r.
  • accept_recorded, refuse_keeps_state, and retry_replays assume copy_p and copy_r: dup(x) == (x, x).
  • refuse_keeps_state now expects Note{Record{p, Refused{r}, at}, Fresh{Refused{r}}}.
  • retried takes the kept state for the Note case.

PROOF.bend proves all five. Bytes.eq reflexivity and the Array.clone copy fact are trust assumptions, listed in the README trust boundary. eq was already one before this change.

Verification

  • scripts/check.sh commands: the laws hold, and check.bend passes. It has a new case: an event with a NUL byte is recorded, its retry replays, and a payload that differs only after the NUL byte is a conflict.
  • scripts/publish.sh --check: [email protected] will publish. scripts/pins.sh passes.
  • Bench: every variant (C, Rust, Bun, Python, Bend) now binds BLOBs. The checksums still agree: 5023800 / 1224217884 at N = 10,000. Bend's times did not change much: 93.5 ms fresh and 43.0 ms replay, against 93.6 and 42.0 before.

Closes #504

…blobs

handle now takes Bytes.Bytes for the payload, state, result, and effect,
and stores them as SQLite BLOBs, so a webhook body with a NUL byte
round-trips and deduplicates. step is generic over affine types: it takes
dup_p and dup_r to copy the payload and result of a new command, and a
refusal answers Note (record only) instead of rewriting the old state.

Closes #504
@paymog
paymog merged commit b173bb5 into main Oct 19, 2026
4 checks passed
@paymog
paymog deleted the feat/commands-bytes-504 branch October 19, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

commands: payloads, results, and state as Bytes blobs

1 participant