# 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: 7f519242e for round 2 (sage-1171). Round 1 was built on c7f3beb17. 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 was c7421cabd, and the two commits after 7f519242e touch 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 `-`, then `-1` through `-9`. A name is free when none of these exist: - `$STATE/stack-rollback/` - `$DATA/backups/stack/` - `$DATA/bus.post-attempt-` 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-$` 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_opts` no 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 calls `usage` outside `--plan`, as before. Under `--plan` it goes through `check_refuse tester-port-invalid`. `tester-port-target` goes through `check_refuse` in 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 `date` stand-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 are `id` and `id-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-failed` with no start or stop and an unchanged fingerprint. Its log line has activation null. - `--restore` with `-0`, `-10`, `-a`, `-` or a 13th hex character appended is usage (exit 4). The base form, `-1` and `-9` are not. - T44, item 2. - `--plan --tester-port P` with `--pm-after-harness` on 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 `--plan` give 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 `--plan` it 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/` already exists for the copy adopt will take. Adopt exits 0, and both the pointer's activation and the one log line are `-1`. - C2, in T44: "without --plan a bad port is usage before any pointer read". With no pointer, `activate C1 acme --tester-port 80` exits 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_business` refuses no-pointer first, with exit 3 and a log line. - 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 ``, expected `-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.sha256` digest c743480eb1e92ee1e1c4365d715543466f58c712a73c8cc39d6147600c1b2c06. - `build.patch` sha256 109b774d43573cabb8044dac4907bdc8fb19295cedd2f7f46f8a9980154f3c45, `git patch-id --stable` 31b1d502444eb4b6589662e6b917600a40030b2f. 161 insertions, 10 deletions, five files. It applies to c7421cabd (`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`.