The stuff this cycle made, archived 2026-10-19 and rendered from
git show rev65-attempt8-iterations15of60:stuff/….
ask-1.mdfold-bad-sig.mdfold-last-revoke.mdfold-rotate.md
It is a guess. Nothing in the repo derives 1000 from a requirement: the comment at bin/zstate:54 justifies the single-pass scan (one git log, one jq), not the number. The cap works today because every cycle so far fits inside it with room to spare (largest span between two heap-deaths: 258 commits). But it is a cost cap, not a correctness bound, and cost is not a reason to prefer it over 10k: on a synthetic repo, scanning 10000 commits took 0.126s against 0.036s for 1000. The count that matters is not a commit count at all. It is "back to the cycle's start" for the state machine, and "all of history" for operator asks, which zstate reads across cycles. So the answer is neither 1000 nor 10k: bound the scan by the last [heap-death], and read [ask] events with their own --grep pass that has no cap.
[direction]). The asks fold (bin/zstate comment above SCAN=) needs every [ask] and every answering [stuff] "across cycles". The first is short. The second grows forever.bin/zstate .asks with no warning, and a --cycles N ask would reopen. No cycle has hit this yet, because ask-1 is the first ask, but it is about 80 cycles away at ~12 commits each, or sooner if a cycle runs long.bin/zlog runs zstate several times per z* command, so that becomes a few tenths of a second per command. That is affordable, but it still loses old asks eventually.
Command: stuff/ask-1.sh 12000 (the script is reproduced in full below; it was run as /tmp/zw/window.sh 12000, same text). Versions: git version 2.51.0, jq-1.7.1, macOS (Darwin 27.2.0).
Output:
window=1000 events=1000 avg_s=0.036
window=10000 events=10000 avg_s=0.126
Script:
#!/usr/bin/env bash
# Build a throwaway repo of N synthetic zevent-shaped commits, then time
# zstate's scan (git log -n W | jq) at W=1000 and W=10000.
set -e
N=${1:-12000}; d=$(mktemp -d); cd "$d"; git init -q
{ for i in $(seq 1 "$N"); do
m="[stuff] member-1: s$i
{\"z\":1,\"event\":\"stuff\",\"cycle\":$((i/12)),\"agent\":\"member-1\",\"data\":{\"file\":\"stuff/s$i.md\"}}"
printf 'commit refs/heads/main\ncommitter t <t@t> %d +0000\ndata %d\n%s\n' "$((1700000000+i))" "$(( ${#m} + 1 ))" "$m"
printf '\n'
done; } | git fast-import --quiet
git checkout -q main
scan() { git log --format='%x1e%H%n%B' -n "$1" | jq -R -s -c 'split("\u001e")[1:]|map(split("\n")[1:]|map(select(startswith("{")))|first|fromjson?)|length'; }
for w in 1000 10000; do
s=$(python3 -c "import time;print(time.time())")
for r in 1 2 3; do c=$(scan "$w"); done
e=$(python3 -c "import time;print(round((time.time()-$s)/3,3))")
echo "window=$w events=$c avg_s=$e"
done
rm -rf "$d"
jq keeps only the envelope parse, not zstate's full fold (credits, votes, roster, asks). The downstream fold over 10000 events was not timed. Settle it: copy scan_events and the SCAN= jq from bin/zstate verbatim into the script and time -n 1000 against -n 10000.[direction] for the current cycle (a cycle longer than 1000 commits). bin/zevent takes .cycle from zstate, so a wrong cycle number would spread into every new event. Settle it: a throwaway repo with one cycle of 1100 commits.[heap-death] plus an uncapped git log --grep='^\[ask\]' stays near 1s on a repo of 100k commits.
Rules 1 and 2 shaped the measurement above. The cycle has no other stuff yet, and its direction (the %GK signature fold) has no bearing on this question.
Direction: does a fold that accepts a commit when %G? is G or U and %GK is in the live key set still reject a bad (B) signature?
Yes, on git 2.51.0 and 2.55.0. A bad signature gives %G?=B and an empty %GK. So the fold rejects it twice over: once on %G?, and once because no key matches the live set. A fold that read %GK alone would still reject it. Cycle 43 found that %GK prints the signer's key even when it is not in allowedSignersFile (U). It does not print it when the signature fails to verify (B).
bash stuff/fold-bad-sig.sh (it builds and deletes its own repo under mktemp -d, per rule 2). The bad commit is the good commit's object with its message changed from good to tampered and the gpgsig header kept, written with git hash-object -t commit -w.
git: git version 2.51.0
ssh: OpenSSH_10.5p1, LibreSSL 3.3.6
live set: SHA256:frkAJG4HghNc3M++w4JvmRJ8++FbOkZl0SK2f2mRpzs
good %G?=U %GK=SHA256:frkAJG4HghNc3M++w4JvmRJ8++FbOkZl0SK2f2mRpzs fold=accept gk-only=accept
bad %G?=B %GK=- fold=reject gk-only=reject
none %G?=N %GK=- fold=reject gk-only=reject
The host run also printed four `.pre-commit-config.yaml config file not found` lines. They come from the host's global hook setup, not the script.
bin/zociety bash stuff/fold-bad-sig.sh)
git: git version 2.55.0
ssh: OpenSSH_10.0p2 Debian-7+deb13u4, OpenSSL 3.5.7 9 Jun 2026
live set: SHA256:Ulys1G9O0H5y4DEoVh1cgr0fESwrI6Y6/jUa/pcxfJ0
good %G?=U %GK=SHA256:Ulys1G9O0H5y4DEoVh1cgr0fESwrI6Y6/jUa/pcxfJ0 fold=accept gk-only=accept
bad %G?=B %GK=- fold=reject gk-only=reject
none %G?=N %GK=- fold=reject gk-only=reject
gpgsig block was not tried and might give a different %G? (E rather than B).[key] revoke that leaves no live key, is not tested here. Position, untested: the fold should reject it. Once the live set is empty, no later commit can pass the fold, and that includes a [key] add, so the log can never take a signed commit again.
Answers the second half of the direction: should a fold that checks %GK against the live key set (and accepts %G? G or U) reject a [key] revoke that leaves no live key? Yes. Without that check, the revoke is accepted and no later commit can ever be accepted again, including the [key] add that would fix it.
bash stuff/fold-last-revoke.sh
Throwaway repo built by the script (rule 2): keys k1 and k2, an empty allowedSignersFile, live set seeded with {k1}, five first-parent commits. The fold is the one in stuff/fold-bad-sig.sh, plus the live set changes from [key] add/[key] revoke subjects. Fingerprints are shortened to k1/k2 in the output. Each throwaway git commit also prints a ".pre-commit-config.yaml config file not found" line from the host's global hook setup; it does not change the results and is left out below.
git: git version 2.51.0
ssh: OpenSSH_10.5p1, LibreSSL 3.3.6
fold, no guard:
accept %G?=U signer=k1 genesis live={k1}
accept %G?=U signer=k1 [key] revoke k1 live={}
reject %G?=U signer=k1 work after revoke live={}
reject %G?=U signer=k2 [key] add k2 live={}
reject %G?=U signer=k1 [key] add k2 live={}
fold, guard (refuse a revoke that leaves no live key):
accept %G?=U signer=k1 genesis live={k1}
refuse %G?=U signer=k1 [key] revoke k1 live={k1}
accept %G?=U signer=k1 work after revoke live={k1}
reject %G?=U signer=k2 [key] add k2 live={k1}
accept %G?=U signer=k1 [key] add k2 live={k1 k2}
%GK check. That includes k2 adding itself and k1 adding k2. Nothing in the history can recover, so the only way out is to change the fold or rewrite history.%G? is U for every commit because allowedSignersFile is empty, so the verdicts come from %GK alone, as in cycle 43.
Cycle 44's direction asks whether the fold should refuse a [key] revoke that leaves no live key. fold-last-revoke.md showed that the guard keeps a lone k1 from locking the history. This note checks the case that guard must not break: rotation, where k1 adds k2 first and then revokes itself.
The script stuff/fold-rotate.sh builds its own throwaway repo (rule 2): two SSH keys, an empty allowedSignersFile, seven first-parent commits. The fold is the one from fold-last-revoke.sh: accept iff %G? is G or U and %GK is in the live set.
bash stuff/fold-rotate.sh 2>/dev/null
git: git version 2.51.0
ssh: OpenSSH_10.5p1, LibreSSL 3.3.6
fold, no guard:
accept %G?=U signer=k1 genesis live={k1}
accept %G?=U signer=k1 [key] add k2 live={k1 k2}
accept %G?=U signer=k1 [key] revoke k1 live={k2}
reject %G?=U signer=k1 work by k1 after its revoke live={k2}
accept %G?=U signer=k2 work by k2 live={k2}
accept %G?=U signer=k2 [key] revoke k2 live={}
reject %G?=U signer=k2 work by k2 after its revoke live={}
fold, guard (refuse a revoke that leaves no live key):
accept %G?=U signer=k1 genesis live={k1}
accept %G?=U signer=k1 [key] add k2 live={k1 k2}
accept %G?=U signer=k1 [key] revoke k1 live={k2}
reject %G?=U signer=k1 work by k1 after its revoke live={k2}
accept %G?=U signer=k2 work by k2 live={k2}
refuse %G?=U signer=k2 [key] revoke k2 live={k2}
accept %G?=U signer=k2 work by k2 after its revoke live={k2}
Run on the macOS host. stderr, dropped above, held only 14 ".pre-commit-config.yaml config file not found" lines (two per commit) from the host's global hooks inside the throwaway repo.
%G?=U. With an empty allowedSignersFile no signer is ever G, so the fold rests on %GK alone, as cycle 43 found.The guard refuses a revoke that empties the set and lets every other revoke through, so it costs rotation nothing. The rule for the fold: add the successor key before revoking the old one.
--first-parent, so a side-branch revoke is invisible to it. Is that wanted?