feat(engine): retire a broadcast with coord-send --retract

Nothing could remove a message from _broadcast/inbox/. coord-done is
directed-only and never touches the broadcast queue, so the backlog could
only grow: every new repo received the entire standing history at its first
session, including announcements that had since become false.

--retract <filename> archives the message into _broadcast/archive/, so no
future repo is served it. Three deliberate limits, all pinned by tests:

- Un-send, not recall. Repos that already received it keep it;
  _broadcast/seen/ is delivery history and is left untouched.
- Only the sender may retract (from: must match the repo identity). --from
  overrides it, as everywhere else in the engine, which makes the check an
  accident guard rather than a security boundary.
- Nothing is deleted, mirroring coord-done. Retracting twice is a no-op.

The branch runs before every send-side validation and before the stdin body
read, since a retract carries no subject and no body.

Selftest 70 -> 82 (new section 19). Also fixes two README defects the
feature exposed: the install command still named coord@ after the v0.3.0
rename, and the docs advised pruning _broadcast/inbox/ by hand, which
contradicted the rule that the script owns mailbox files.
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 15:11:47 +02:00
commit 316b8acdd2
8 changed files with 184 additions and 15 deletions

View file

@ -235,6 +235,49 @@ printf '%s' "$aerr" | grep -q 'unknown argument: --bogus-flag'; check "inbox: un
aout="$("$INBOX" --repo argrepo --bogus-flag 2>/dev/null)"; rc=$?
[ "$rc" -eq 0 ] && printf '%s' "$aout" | grep -q 'ARG-BODY'; check "inbox: unknown argument still reads the inbox and exits 0" $?
# 19. A broadcast can be retired. Without this, the broadcast backlog grows
# monotonically: every NEW repo receives the whole history at its first session,
# including announcements that have since become false. Retract is sender-side
# ("un-send"), archives rather than deletes, and is NOT recall - repos that
# already received the message keep it. Every invocation reads from /dev/null so
# a regression that falls through to the stdin body read fails instead of hanging.
"$SEND" --broadcast --from retractor --subject "oops" --message "RETRACT-ME-BODY" >/dev/null
rbase="$(basename "$(ls "$CLAUDE_COORD_DIR"/_broadcast/inbox/*-from-retractor.md 2>/dev/null | head -1)")"
"$SEND" --broadcast --from keeper --subject "keep" --message "KEEP-BC-BODY" >/dev/null
nr="$("$SEND" --retract "$rbase" --from someone-else </dev/null 2>&1)"; rc=$?
[ "$rc" -eq 2 ] && printf '%s' "$nr" | grep -q 'sent by retractor'; check "retract by a non-sender is refused" $?
[ -e "$CLAUDE_COORD_DIR/_broadcast/inbox/$rbase" ]; check "refused retract leaves the broadcast pending" $?
vo="$("$SEND" --retract </dev/null 2>&1)"; rc=$?
[ "$rc" -eq 2 ] && printf '%s' "$vo" | grep -q 'requires a value'; check "retract: missing value is a usage error" $?
uo="$("$SEND" --retract no-such-broadcast.md --from retractor </dev/null 2>&1)"; rc=$?
[ "$rc" -eq 2 ] && printf '%s' "$uo" | grep -q 'not found'; check "retract of an unknown broadcast exits 2" $?
bad_rc=0
for bad in . .. sub/dir; do
bo="$("$SEND" --retract "$bad" --from retractor </dev/null 2>&1)"; brc=$?
{ [ "$brc" -eq 2 ] && printf '%s' "$bo" | grep -q 'invalid --retract'; } || bad_rc=1
done
[ "$bad_rc" -eq 0 ]; check "retract rejects . / .. / path names as invalid" $?
comb_rc=0
for combo in --broadcast "--to:x" "--reply-to:y"; do
cflag="${combo%%:*}"; cval="${combo#*:}"
if [ "$cval" = "$combo" ]; then
co="$("$SEND" --retract "$rbase" "$cflag" --from retractor </dev/null 2>&1)"; crc=$?
else
co="$("$SEND" --retract "$rbase" "$cflag" "$cval" --from retractor </dev/null 2>&1)"; crc=$?
fi
{ [ "$crc" -eq 2 ] && printf '%s' "$co" | grep -q 'cannot be combined'; } || comb_rc=1
done
[ "$comb_rc" -eq 0 ]; check "retract cannot be combined with --to/--broadcast/--reply-to" $?
ro="$("$SEND" --retract "$rbase" --from retractor </dev/null 2>&1)"; rc=$?
[ "$rc" -eq 0 ]; check "retract by the sender exits 0" $?
[ ! -e "$CLAUDE_COORD_DIR/_broadcast/inbox/$rbase" ]; check "retracted broadcast leaves the broadcast inbox" $?
[ -e "$CLAUDE_COORD_DIR/_broadcast/archive/$rbase" ]; check "retracted broadcast is archived, never deleted" $?
fout="$("$INBOX" --repo fresh-reader)"
[ "$(printf '%s' "$fout" | grep -c 'RETRACT-ME-BODY')" -eq 0 ]; check "retracted broadcast is never delivered to a new repo" $?
printf '%s' "$fout" | grep -q 'KEEP-BC-BODY'; check "unretracted broadcasts still reach that repo" $?
io="$("$SEND" --retract "$rbase" --from retractor </dev/null 2>&1)"; rc=$?
[ "$rc" -eq 0 ] && printf '%s' "$io" | grep -q 'already retracted'; check "re-retracting an already retracted broadcast is a no-op (exit 0)" $?
echo "----"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]