Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
12 KiB
Row 180: activation ids and plan-mode refusals
Brief: "# Brief: activation ids and plan-mode refusals (row A)" in
docs/plans/2026-10-20_stack-release-after-window.md (blob aafa471ae, c7f3beb17). Gitea #1678, Vikunja P19 391,
plus N1 and N2 from Dewey's row 176 review. Owner Darkwing, reviewer Dewey, gate owner Sage.
Files
| File | What |
|---|---|
build.patch |
the change, git diff 7f519242e over the five paths below |
candidate-manifest.sha256 |
one line per changed path |
- Base:
7f519242efor round 2 (sage-1171). Round 1 was built onc7f3beb17. No commit in c7f3beb17..7f519242e touches the five paths, and the patch is byte for byte the same on either base. At 03:29Z, refactor wasc7421cabd, and the two commits after7f519242etouch only plans and SESSIONS. - Changed:
scripts/stack-release.sh,scripts/webui-service.sh,packages/cli/tests/stack-release.test.mjs,packages/webui/tests/service.test.mjs,docs/TOOLS.md. These are the brief's touched files, with nothing else. - Digests and patch-id: see "Candidate" at the end.
Item 1: activation ids within one second (P19 391)
next_activation() takes the UTC second once and tries the base id <second>-<TO:0:12>, then -1 through
-9. A name is free when none of these exist:
$STATE/stack-rollback/<id>$DATA/backups/stack/<id>$DATA/bus.post-attempt-<second><suffix>
It sets ACT and POST from the same suffix. With all ten taken it refuses capture-failed
("every activation id for X is taken; nothing was stopped") and clears ACT and POST first, so the refused log
line has activation null. new_activation() calls it and sets RS, and do_adopt calls it in place of its own
date line. All three callers run after lock(): activate and rollback reach it only past the --plan exit,
and adopt and pm-on lock first.
Why a suffix and not milliseconds. The brief allowed either. With milliseconds, the brief's pinned-clock test can't produce two runs in one stamp, and the shape of every id would change. With the suffix, the base form is still a whole id, and lexical order is still time order: X < X-1 < ... < X-9 < the next second. Sage approved it (sage-1155).
A side effect: the base code took POST from a second date call, which could land in the next second from
ACT. POST now uses ACT's second.
--restore (:78) now takes ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$.
Check (b): everything that builds or parses an activation id (sage-1155)
git grep over the tree outside v1/, agents/ and docs/plans for stack-active, stack-activation-log,
stack-rollback, backups/stack and post-attempt finds three files: scripts/stack-release.sh, its test
and docs/TOOLS.md. Line numbers are at the candidate.
| Hit | What it does with the id | Result |
|---|---|---|
stack-release.sh:50 log_line |
writes ACT into each log line | fine, writes only |
:100-110 read_pointer |
checks typeof p.activation==="string", sets P_ACT |
fine; P_ACT is read nowhere else |
:116 write_pointer |
writes ACT | fine |
:128 interrupted() |
keys open runs on the exact activation string | fine; a suffixed id is a distinct key |
:331 backup_store, :355 restore_store |
take the backup dir path, never parse it | fine |
:78 --restore regex |
parsed the old form only | changed, takes both |
:827, :883 do_rollback --restore path |
builds $DATA/backups/stack/$RESTORE |
fine, a name only |
| test :378 T1 | ^\d{8}T\d{6}Z-<B12>$ on an adopt id |
fine, one run, no collision |
| test :558 T13 | post-attempt name \d{8}T\d{6}Z |
fine, one run |
| test :678 T24 | capture-failed message with the base id | fine, the base form is still first |
test :274 posts() |
startsWith('bus.post-attempt-') |
fine |
| test :950 T34 | notEqual between two runs' ids |
now always true, which is what the test wanted |
| TOOLS.md :382-433 | the pointer, rollback sets, backups, post-attempt and --restore | updated |
Unrelated hits I checked: packages/cli/tests/notifier.test.mjs:255, a torn-files regex. scripts/release.sh
and packages/runs write their own activation-log.jsonl for the other release system.
Item 2: plan-mode refusals in console_for_target (N1)
parse_optsno longer checks the tester port inline. After the loop it checks the port unless PLAN=1. An empty--tester-port ""is still usage, through a new[ -n "$2" ].- In
console_for_target, an invalid port callsusageoutside--plan, as before. Under--planit goes throughcheck_refuse tester-port-invalid.tester-port-targetgoes throughcheck_refusein both modes. Outside--plan, check_refuse is refuse, so the output is unchanged.
Deviation from the brief (sage-1155)
The brief says the bad port "gives the same reason text usage gives today". Usage has no reason line. It prints the usage block and exits 4. Sage ruled that this was a brief error and that I should use a reason line:
- main Console port 7332:
--tester-port 80 is not a whole port from 1024 to 65535 other than 7332 - otherwise:
... other than 7332 and the Console's --port N
Without --plan the output is still the usage block and exit 4, byte for byte. Sage won't re-pin the brief for
this.
Item 3: webui-service tester port length (N2)
tester_port_ok checks ${#TESTER_PORT} -le 5 on the value as written, before it strips zeros. That's the
count serve uses (webui cli.mjs:30, /^\d{1,5}$/) and the one valid_tester_port in stack-release.sh uses.
007333 now refuses with exit 4, and 07333 still renders.
Tests
- T43 (stack-release.test.mjs), item 1. A
datestand-in on PATH pins every call to one second, except calls with%s(the gate's start marks), which go to/usr/bin/date. Pinning those too would make two gates' journal windows overlap.- Activate C1 with
--pm-after-harness, then pm-on on the same copy. The ids areidandid-1, with matching log lines, two rollback sets and a SHA256SUMS in each. - Two migrate-and-restore rounds. Round 1 restores an old-form id and round 2 a suffixed one. Each prints its own post-attempt path. At the end there are two posts, two backups and four rollback sets.
- Ten names taken: eight rollback sets, one backup and one post-attempt store. The run refuses
capture-failedwith no start or stop and an unchanged fingerprint. Its log line has activation null. --restorewith-0,-10,-a,-or a 13th hex character appended is usage (exit 4). The base form,-1and-9are not.
- Activate C1 with
- T44, item 2.
--plan --tester-port Pwith--pm-after-harnesson a business file that sets harness. P is 80 and the Console's port, against a pointer with--port, then 80 and 7332 against one without. Each case lists exactly the tester-port-invalid line and the pm-after-harness-set line, exits 3 and leaves stderr empty. The same flags without--plangive exit 4, empty stdout and stderr equal to--help's. Nothing starts or stops, and the log doesn't grow.- Activate A, whose Console lacks
--tester-port, with--plan. It lists tester-port-target and unknown-kind-for-target and exits 3. Without--planit refuses tester-port-target with today's message and one log line.
- Round 2 added two subtests for Dewey's round 1 (#1678 comment 29352).
- C1, in T43: "adopt takes the next free id too". The clock is pinned, there's no pointer, and
stack-rollback/<id>already exists for the copy adopt will take. Adopt exits 0, and both the pointer's activation and the one log line are<id>-1. - C2, in T44: "without --plan a bad port is usage before any pointer read". With no pointer,
activate C1 acme --tester-port 80exits 4, with empty stdout, stderr equal to--help's, no log line and no start or stop. Without the post-loop check (:85),pointer_for_businessrefuses no-pointer first, with exit 3 and a log line.
- C1, in T43: "adopt takes the next free id too". The clock is pinned, there's no pointer, and
- service.test.mjs, item 3: "tester service counts the digits as written before it strips leading zeros".
Runs
All runs used the worktree ~/darkwing-scratch/r180/wt at base c7f3beb17, with node_modules linked. Totals
come from the ℹ summary lines.
Run 1, slot 02:40:51.625Z to 02:44:44.402Z (sage-1155):
| Run | Result | End |
|---|---|---|
| stack-release.test.mjs whole | 106 pass, 0 fail | 02:44:22.823Z |
| service.test.mjs whole | 28 pass, 0 fail | 02:44:25.286Z |
m1 no suffix (for s in "") |
killed, T43: second run refuses capture-failed | 02:44:31.894Z |
| m1b old --restore regex | killed, T43: suffixed --restore is usage | 02:44:39.467Z |
m2 invalid port ends --plan (usage) |
killed, T44: exit 4, expected 3 | 02:44:41.429Z |
m2b refuse tester-port-target |
killed, T44: stderr not empty under --plan | 02:44:44.247Z |
| m3 length line removed in webui-service.sh | killed: 007333 exit 0, expected 4 | 02:44:44.402Z |
Defect found during the build
After run 1, while writing this packet, I found that the ten-names refusal wrote the -9 id, which belongs to
another run, into its refused line. lock() refuses interrupted-run before this point (:562), so the line
couldn't close an open run, but a refusal record named a run that wasn't its own. The fix clears ACT and POST
before that refusal. The ten-names subtest in T43 tests it, and Sage agreed with the fix (sage-1159). Run 2
covers the fix and adds two mutants, on the backups/stack check and the post-attempt check. Those prove the
brief's "chosen while holding the lock, against both stack-rollback/ and backups/stack/", which run 1 didn't
cover.
Run 2, slot 02:47:46.563Z to 02:51:59.884Z (sage-1159):
| Run | Result | End |
|---|---|---|
| stack-release.test.mjs whole | 107 pass, 0 fail (run 1 plus the ten-names subtest) | 02:51:22.962Z |
| m1 no suffix | killed, T43: second run refuses capture-failed | 02:51:29.999Z |
m1c ACT="" POST="" removed |
killed: activation ...-9, expected null |
02:51:39.603Z |
| m1d no backups/stack check | killed: the run took the -8 name and captured |
02:51:49.775Z |
| m1e no post-attempt check | killed: the run took the -9 name and captured |
02:51:59.883Z |
service.test.mjs and scripts/webui-service.sh didn't change after run 1. Each mutant edited the worktree file
after a backup checked by sha256, and the script put the file back and checked the sha again.
No other test file runs the changed code. packages/webui/tests/pinned-copy.test.mjs and
packages/cli/tests/stack-copy.test.mjs run webui-service.sh copy, which doesn't reach tester_port_ok. No
test pins a sha256 of either script. Sage's gate runs the suites.
Round 2, slot 03:24:32.105Z to 03:28:20.029Z (sage-1171), worktree on 7f519242e:
| Run | Result | End |
|---|---|---|
| stack-release.test.mjs whole | 109 pass, 0 fail (run 2 plus C1 and C2) | 03:28:05.528Z |
X2 adopt's own date line back in place of next_activation |
killed, C1: activation <id>, expected <id>-1 |
03:28:16.516Z |
| X1 post-loop port check (:85) removed | killed, C2: exit 3 no-pointer, expected 4 | 03:28:20.029Z |
Dewey said nothing else needed re-running. service.test.mjs and both scripts are unchanged since run 2.
Dewey's N1 is a note only and isn't changed in this round. Under --plan, a hard refusal before
console_for_target, such as no-pointer, ends the run before the port is checked, so tester-port-invalid
isn't on that plan.
Candidate
Round 2:
candidate-manifest.sha256digest c743480eb1e92ee1e1c4365d715543466f58c712a73c8cc39d6147600c1b2c06.build.patchsha256 109b774d43573cabb8044dac4907bdc8fb19295cedd2f7f46f8a9980154f3c45,git patch-id --stable31b1d502444eb4b6589662e6b917600a40030b2f. 161 insertions, 10 deletions, five files. It applies toc7421cabd(git apply --check).- Only the test file changed from round 1. Round 1 was manifest digest 26ea8e88d23a and patch-id 30a272d49b79.
| Path | sha256 |
|---|---|
| docs/TOOLS.md | 3468a2d2eeeb0175dc37499e89e609144ff3a07bb275fea64b82bbdbd64e4de5 |
| packages/cli/tests/stack-release.test.mjs | 09c6cdd7568cd6769d9a39c7f5655df1850b1de7e01712fac0bad84ff2b692d5 |
| packages/webui/tests/service.test.mjs | 1fca5facd9be5014803cef88a4f79d51c7b67920bdf001a1909daa2a1339fa03 |
| scripts/stack-release.sh | f225cfdf5d2ba3f828b45c8af3c0da7f7f9d8722daa6890c374e4253f3ee37a7 |
| scripts/webui-service.sh | 0636852303b73aa9ad4761679bb7c7bda4eca3e4ff56d5f67d2cb1d1adc75b80 |
The patch adds no em dash. BUILD.md and the TOOLS.md lines pass tools/unslop-hook/unslop-check.js.