diff --git a/docs/v4.0.0/MESH.md b/docs/v4.0.0/MESH.md index 4b8a4535..ba9030d6 100644 --- a/docs/v4.0.0/MESH.md +++ b/docs/v4.0.0/MESH.md @@ -492,34 +492,41 @@ ideas." ### 7c.2 The engine -Two more addresses follow a node's ports (section 4.1, 7a): +More addresses follow a node's ports (section 4.1, 7a): | Address | What | |---|---| -| `base + V4_PORTS + 4` | Offer to: store the number of a port | -| `base + V4_PORTS + 5` | Offer: store the word offered. Fetch: the wait | +| `base + V4_PORTS + 4 + k` | Offer on port *k*: store the word offered there | +| `base + 2 * V4_PORTS + 4` | The wait: a fetch here blocks | -- A store to either is remembered and waits for nothing. +*(Changed while the plan was written, 2026-10-07: as first approved there +was one offer, on one port. A node may owe NACKs to several neighbours at +once, and with one offer the node would sleep offering the first to a +neighbour that is stuck while a second neighbour waited for its own. So +a node may offer a word on each of its ports at once.)* + +- A store to an offer address is remembered and waits for nothing. - **A fetch of the wait blocks the node** until one of these, which the fabric looks for once a step, in this order: 1. *A word for it.* A neighbour is blocked writing to it, on any port, or a device on one of its ports has a word to give. It gets that word, as a read of "any port" would, and "which port" says where - from. Its offer is withdrawn. - 2. *Its offer taken.* The node on the port offered to is blocked reading - that port or any port; or is a device that takes what is written; or - is itself in the wait. The word is handed over. The fetch gives 0, - and "which port" gives `V4_PORTS`, which is no port: the offer was - taken. + from. + 2. *An offer taken.* The node on a port it has an offer on is blocked + reading that port or any port; or is a device that takes what is + written; or is itself in the wait. The word is handed over. The + fetch gives 0, and "which port" gives `V4_PORTS + k`: the offer on + port *k* was taken. The lowest such port is the one. 3. *Two nodes each offering to the other:* the one at the lower place in the fabric has its offer taken, and the other gets the word (case 2 for the one, case 1 for the other). +- Whichever it is, every offer the node had is withdrawn. - A node in the wait that is handed a word by case 2 of another node gets it by its own case 1. - A node in the wait executes nothing and spends no heat. It shows to its neighbours as waiting to read. -- With nothing on the port offered to, a node that can take an error has - error 18 raised on it (section 4.1); a bare node waits. A neighbour that +- With nothing on a port it has an offer on, a node that can take an error + has error 18 raised on it (section 4.1); a bare node waits. A neighbour that is asleep takes nothing: the offer stands. - `v4_fabric_interrupt_error`, a device's `pending`, and `v4_node_words_since_look` are removed. @@ -533,9 +540,10 @@ Two more addresses follow a node's ports (section 4.1, 7a): instead, take that message in and offer again; when the offer is taken, write the rest, each word waiting for the neighbour, which has taken the first and reads to the end. -- *Paying what is owed* (`(PAY)`): each NACK is offered the same way. A - node with nothing to do that owes one sleeps in the wait; it no longer - goes round, and it no longer writes one without looking. +- *Paying what is owed* (`(PAY)`): every NACK owed is offered at once, + each on the port it goes by (one to a port). A node with nothing to do + that owes one sleeps in the wait; it no longer goes round, and it no + longer writes one without looking. - *Text from its console for a node that is waiting to begin a message* ends what the node was doing in error 21, "Interrupted", and is then done, as in `AWAIT` (7b.6). This is how Hera is typed to while she waits diff --git a/docs/v4.0.0/plans/2026-10-07-step6d-the-wait.md b/docs/v4.0.0/plans/2026-10-07-step6d-the-wait.md new file mode 100644 index 00000000..97863170 --- /dev/null +++ b/docs/v4.0.0/plans/2026-10-07-step6d-the-wait.md @@ -0,0 +1,205 @@ +# Step 6d: The Wait — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** A node that is stuck holds up no other node. + +**Architecture:** The engine gains a wait: a node offers the first word of a message on one or more ports and sleeps until an offer is taken or a word comes for it; the fabric does the handing over in one step. The nucleus begins every message through it (`(GATE)`), pays what it owes through it (`(PAY)`), and so never writes to a node that is not ready and never goes round looking. The lower-number rule, the looking-again, and the engine's console release are removed. + +**Tech Stack:** C99 (`v4/src`, `v4/system/boot.c`, tests), the v4 nucleus dialect (`v4/capsule/core.v4`, `quit.v4`), `make -C v4`, `make -f kernel/Makefile`. + +**Spec:** `docs/v4.0.0/MESH.md` section 7c; acceptance in 7c.5. + +## Global Constraints + +- Branch `StarForth-v4.0.0`, main checkout. No new branch, no stash, no worktree. Commit and push after every task; end messages with `Co-Authored-By: Claude Opus 5.5 `. +- No bad code is released: a defect found on the way is fixed, test first, and reported. No stubs or stand-ins. Do not change v3. +- The F18 is borrowed ideas, not a rule: a difference from it is noted in a line, not held for ruling. A difference from v3 is still shown first. +- Port addresses, word address `base` = port 0: `base + V4_PORTS` any; `+ 1` which port; `+ 2` writers; `+ 3` readers; `base + V4_PORTS + 4 + k` offer on port k; `base + 2 * V4_PORTS + 4` the wait. +- "Which port" after the wait: `k` (0 .. `V4_PORTS - 1`) a word came from port k; `V4_PORTS + k` the offer on port k was taken, and the fetch gave 0. +- Errors: 18 "No one on that port", 21 "Interrupted". Both exist. +- Every test is watched failing before the code that passes it is written. A test that passes at once is shown to fail with the code taken out, and that is said in the ledger. +- Before any QEMU run read `.claude/CLAUDE.md` "Running / Acceptance" and the memory note `acceptance-test-rules.md`. One QEMU at a time, `clean` before `qemu`, all three ISAs, logs kept and committed. Never delete a log. + +## Review Focus + +1. **Two NACKs owed, the first to a stuck node, the second to a node that is waiting for it.** Expected: the second is paid; the node that waits gets "Message refused". Test in Task 2. +2. **An offer that could be taken in the same step a word comes for the node.** Expected: the word comes first, the offer is withdrawn, and nothing is handed over twice. Test in Task 1. +3. **Three nodes in a ring, each offering to the next.** Expected: every message arrives; all three come to rest. Test in Task 1. +4. **Console text for another node reaching a node whose line is waiting to begin a message.** Expected: it is kept and does not interrupt; only text for that node does. Test in Task 2. +5. **An offer to a neighbour that is asleep, then woken.** Expected: the offer stands while it sleeps and is taken when it wakes and reads. Test in Task 1. + +--- + +### Task 1: The wait in the engine + +**Files:** Modify `v4/include/v4/node.h`, `v4/src/node.c`, `v4/src/fabric.c`, `v4/include/v4/fabric.h`; Test `v4/tests/test_fabric.c`. + +**Interfaces — Produces:** +- `#define V4_PORT_OFFER (V4_PORTS + 4u)` (index of the offer on port 0), `#define V4_PORT_WAIT (2u * V4_PORTS + 4u)`. +- Node fields: `unsigned offers;` (bit k: a word is offered on port k), `v4_cell offer[V4_PORTS];`, `int waiting;` (blocked in the wait). +- `int v4_node_in_wait(const v4_node *n);` 1 if blocked in the wait with nothing given. +- `int v4_node_offer_taken(v4_node *n, unsigned k);` the offer on port k is taken: the node is given 0 from `V4_PORTS + k`, every offer withdrawn. Returns 0 if it had none there. +- `int v4_node_offer_as_write(v4_node *n);` for a host with one node whose devices always take: if the node is in the wait with an offer, it becomes a blocked write of that word on the lowest offered port (`asking`, `request`, `ask_port`), and `v4_node_port_served` then completes the wait as `v4_node_offer_taken` would. Returns 1 if it did. +- Nothing is removed in this task. + +- [ ] **Step 1: Write the failing tests** at the end of `test_fabric.c`, each a small assembled programme as the existing ones are (`loaded`, `LIT`, `O`, `ND`): + - *Taken by a reader:* a stores 7 to its offer on port 0 and fetches the wait, storing the fetched value at `OUT` and "which port" at `OUT + 1`; b reads its port 3. After settling: b has 7; a has 0 and `V4_PORTS + 0`. + - *Taken by a device:* the same with a device that takes on port 0: the device got 7. + - *A stuck neighbour:* b counts for ever. After 200 steps a is `v4_node_in_wait`, has executed nothing since (its `es` clock is unchanged over the last 100 steps), and b's readers word shows a as waiting to read. + - *Withdrawn by an incoming word (Review Focus 2):* a offers to stuck b on port 0; c, wired to a's port 2, writes 9. a gets 9 and "which port" 2; its `offers` is 0; b was given nothing. + - *Offer and reader in the same step:* a offers to b; b reads; c writes to a, all arranged to block in the same step: a gets c's word, b is given nothing that step, c is served, and after a offers again b gets the word exactly once. + - *Two offers facing:* a and b each offer to the other. The one added first has `V4_PORTS + k`; the other has the word and the port it came by; neither has an offer left. + - *A ring (Review Focus 3):* a offers to b, b to c, c to a; each on waking with a word reads no more, and on its offer being taken stops. All three come to rest; each word offered was either delivered once or is still offered by a node in the wait, and no word was delivered twice. + - *Asleep (Review Focus 5):* b asleep and reading: a's offer stands for 100 steps; `v4_fabric_wake`, and it is taken. + - *An empty port:* a offers on a port with nothing wired. With no error attached it stays in the wait. With `v4_fabric_gone_error(&f, 18)`, `v4_node_error_attach` and `v4_node_fault_attach`: one step, error 18 is raised, `offers` is 0. + - *The lone-node helper:* a node alone, in the wait with offers on ports 1 and 3: `v4_node_offer_as_write` gives `asking`, `ask_port == 1`, `request` the word offered there; `v4_node_port_served` then leaves it given 0 from `V4_PORTS + 1` and not waiting. +- [ ] **Step 2: Run** `make -C v4 run-64-test_fabric.c`. Expected: it does not compile, naming `v4_node_in_wait` and `V4_PORT_OFFER` as undeclared. Then add the two `#define`s, the fields and the three declarations only, with bodies that return 0, and run again. Expected: it compiles and every new check fails (nothing is ever handed over). Those bodies are replaced in Step 3; none is left. +- [ ] **Step 3: Implement.** + +`node.h`, in `v4_node` after `since_look`: +```c + unsigned offers; /* bit k: a word is offered on port k (the wait: MESH.md 7c) */ + v4_cell offer[V4_PORTS]; + int waiting; /* non-zero: blocked in the wait */ + int offer_asked; /* non-zero: `asking` stands for the offer on `ask_port` (v4_node_offer_as_write) */ +``` +`node.c`: +```c +int v4_node_port_index(const v4_node *n, v4_cell addr) +{ + if (n->port < 0 || addr < n->port || addr > n->port + (v4_cell)V4_PORT_WAIT) return -1; + return (int)(addr - n->port); +} +``` +In `v4_node_store`, before the "any and which port are not written to" fault: +```c + if (port >= (int)V4_PORT_OFFER && port < (int)V4_PORT_WAIT) { + unsigned k = (unsigned)port - V4_PORT_OFFER; + n->offer[k] = value; + n->offers |= 1u << k; + return; + } +``` +In `v4_node_fetch`, with the other port indexes (the wait is fetched as "any" is): +```c + if (port == (int)V4_PORT_WAIT) { + if (!n->given) return 0; + n->given = 0; + n->last_from = n->given_port; + return n->given_value; + } + if (port >= (int)V4_PORT_OFFER) return 0; /* an offer is not read back */ +``` +`v4_node_read_ready`: +```c +int v4_node_read_ready(v4_node *n, v4_cell addr) +{ + int port = v4_node_port_index(n, addr); + if (port < 0 || (port > (int)V4_PORT_ANY && port != (int)V4_PORT_WAIT)) return 1; + if (n->given && (port >= (int)V4_PORT_ANY || (unsigned)port == n->given_port)) return 1; + n->reading = 1; + n->read_port = port == (int)V4_PORT_WAIT ? V4_PORT_ANY : (unsigned)port; + n->waiting = port == (int)V4_PORT_WAIT; + return 0; +} +``` +`v4_node_port_give` also sets `n->waiting = 0; n->offers = 0;` only when `n->waiting` was set (a plain read leaves offers alone). New: +```c +int v4_node_in_wait(const v4_node *n) { return n->waiting && n->reading && !n->given; } + +int v4_node_offer_taken(v4_node *n, unsigned k) +{ + if (!v4_node_in_wait(n) || k >= V4_PORTS || !(n->offers & (1u << k))) return 0; + v4_node_port_give(n, V4_PORTS + k, 0); + return 1; +} + +int v4_node_offer_as_write(v4_node *n) +{ + unsigned k; + if (!v4_node_in_wait(n) || n->offers == 0) return 0; + for (k = 0; !(n->offers & (1u << k)); k++) { } + n->request = n->offer[k]; n->ask_port = k; n->asking = 1; n->offer_asked = 1; + return 1; +} +``` +`v4_node_port_served`: if `n->offer_asked`, clear it and `asking`, then `v4_node_port_give(n, V4_PORTS + n->ask_port, 0)`; otherwise as now. `v4_node_port_gone`: a node in the wait with `offers` set has `reading`, `waiting`, `offers` cleared and the error raised. `v4_node_port_attach` and `v4_node_reset` zero the new fields. + +`fabric.c`, in `v4_fabric_step` after the two hand-over loops and before the error loops (so a word for the node comes first): +```c + /* the wait (MESH.md 7c): an offer is taken by whoever is ready for it */ + for (i = 0; i < f->capacity; i++) { + v4_node *n; + unsigned k; + if (!awake(f, i)) continue; + n = &f->place[i].node->n; + if (!v4_node_in_wait(n)) continue; + for (k = 0; k < V4_PORTS; k++) { + const v4_wire *w = &f->place[i].wire[k]; + if (!(n->offers & (1u << k))) continue; + if (w->kind == V4_WIRE_DEVICE) { + if (w->device->take && w->device->take(w->device->self, n->offer[k])) { (void)v4_node_offer_taken(n, k); done++; break; } + } else if (w->kind == V4_WIRE_NODE && awake(f, w->node)) { + v4_node *r = &f->place[w->node].node->n; + if (r->reading && !r->given && (r->read_port == w->port || r->read_port == V4_PORT_ANY)) { + v4_cell word = n->offer[k]; + (void)v4_node_offer_taken(n, k); + v4_node_port_give(r, w->port, word); + done++; + break; + } + } + } + } +``` +(`r` in the wait is reading "any", so it takes the word and its own offers are withdrawn: the lower place writes.) In the gone-error loop add: a node in the wait with an offer on a port wired to nothing. +- [ ] **Step 4: Run** `make -C v4 run-64-test_fabric.c run-32-test_fabric.c`, then `make -C v4 -k test` and `make -C v4 -k sanitize`. Expected: 0 failures everywhere; nothing else uses the new addresses yet. +- [ ] **Step 5: Mutation check.** Move the offer loop above the hand-over loops: the "withdrawn by an incoming word" or "same step" check fails. Put it back. +- [ ] **Step 6: Commit** `feat(v4.0.0): the wait in the engine -- a node offers a word and sleeps` and push. + +### Task 2: The nucleus begins every message through the wait + +**Files:** Modify `v4/capsule/core.v4` (`(GATE)`, `(PAY)`, `(PAY1)`, `(FLUSH-OUT)`, the 7a comment block), `v4/capsule/quit.v4` (`(IDLE)`, `(PASS-ON)`, `(FINISH)`, `(SEND1)`, `GONE`, `(RAISED)`), `v4/tests/host_map.h` (`(GATE-WORD)`, the constants for the offer and wait addresses), `v4/tools/mkimage.c` if it lists constants, `v4/system/boot.c`, `v4/tests/test_host_quit.c`; Test `v4/tests/test_host_mesh.c`. + +**Interfaces — Consumes:** Task 1's addresses and `v4_node_offer_as_write`. **Produces:** +- `(OFFERS) ( -- flag )` reads the wait once. Zero: a word came instead, and that whole message has been taken in and kept (`(TAKE)`), its seven words still in `(MQ-HDR)`. Non-zero: an offer was taken; B is at the port it was taken on, and `(GATE-PORT)` holds that port's address. +- `(GATE) ( w port -- )` the word is offered on the port, by its address, until it is taken; B is left at the port. Its callers no longer write the first word themselves. +- `(PAY) ( -- flag )` offers every NACK owed, one to a port, reads the wait once, and if one was taken writes the rest of it and marks it paid. Zero if nothing is owed. + +- [ ] **Step 1: Write the failing tests** in `test_host_mesh.c`. Replace the last section ("THE LIMIT") and the two before it that are about a stuck node with these; the fabric keeps `v4_fabric_gone_error(&f, V4_ERROR_NO_ONE)` from the start of the section. + - *Higher creditor stuck (acceptance 4):* node 12 sends 55 a message by node 10, which has no way, and is then stuck in `BEGIN 0 UNTIL`. Node 10 owes it a NACK. `tell(10, "1 2 + .")` gives "3 "; `tell(11, "20 22 + .")` gives "42 ": node 10 goes on, and so does what passes through it. Node 10 is not `asking`. + - *Lower creditor stuck:* node 11 sends 55 by node 12, which has no way, and is stuck. Node 12 owes it. `tell(12, "7 8 * .")` gives "56 ", and node 12 between lines is `v4_node_in_wait` (it sleeps: its `es` clock does not move over 1000 steps). + - *A third node writes to the one that owes:* with node 11 stuck and node 12 owing it, node 10 sends node 12 text and gets its answer. + - *Two owed (Review Focus 1):* node 10 owes one to stuck node 12 and then one to node 11, which sent 66 a message and is in `66 AWAIT`. Node 11's line ends "Message refused". + - *A message for the stuck node:* node 10 does `: TO12 S" 1 DROP" 12 SEND 65 EMIT ; TO12` with 12 stuck: still running, node 10 `v4_node_in_wait`. Text from the console for node 11 then passes through and is answered, and node 10's line is still waiting (Review Focus 4). Text from the console for node 10 ends that line "Interrupted", "A" is never printed, and the new line is done. + - *Removed:* node 12, stuck, is removed while node 10 offers to it: node 10's line ends "No one on that port" (acceptance 6), and it owes nothing. + - The existing flood ("two hundred from each to each") stays as it is: it must still come to rest (acceptance 7). Its counts of arrived and lost may change; the check that `arrived + lost` accounts for every message sent stays exact. +- [ ] **Step 2: Run** `make -C v4 run-64-test_host_mesh.c`. Expected: "higher creditor stuck" fails with node 10 `asking`; "lower creditor stuck" fails on the clock moving; the Hera-line check fails with node 10 `asking` and no "Interrupted" (the mesh fabric has no interrupt error set). +- [ ] **Step 3: Implement the nucleus.** + - Constants `(OFFER)` = `(PORT) + PORTS + 4` and `(WAIT)` = `(PORT) + 2 * PORTS + 4`, where the port constants are defined. + - `(OFFERS)`: `(WAIT) b! @b` gives the word; `(PORT)+9 b! @b` gives which. If which is below `PORTS`: the pair is `( to port )` as in `(IDLE)`'s read of "any": B to `(PORT) + which`, `(TAKE)`, leave 0. Otherwise store `which - PORTS` in `(GATE-PORT)` as an address `(PORT) + k`, put B there, drop the 0 that was fetched, leave -1. + - `(GATE)`: store the port in `(GATE-PORT)` and the word in `(GATE-WORD)`, because it runs with the stack as full as `EMIT` may be. Loop: store `(GATE-WORD)` at `(OFFER) + port number`; `(OFFERS)`; if taken, return. If not: when `(QUIET)` is 0 and the message just kept is text (type 1) for this node from `(A-CONSOLE)`, store 21 in `NODE-ERROR`; otherwise loop. With nothing on the port the engine raises 18 itself. + - The five callers drop their own first write: `(FINISH)` `(MSG)+1 a! @ (DONE-PORT) a! @ (GATE)`; `(PASS-ON)` `(MSG) a! @ port (GATE)` then words 1 to 6 with `(MSG)+1 a! 5 FOR @+ !b UNEXT`; `(SEND1)`, `GONE`, `(FLUSH-OUT)` likewise. + - `(PAY)`: for k from 0 to `(OWED#) - 1`: `(PORT-FOR)` of its `to`; no way, or `(THERE)` false: count in `(LOST)` and `(PAID)` (as now). Otherwise, if no offer has been stored on that port in this pass (a bit kept on the stack or in a cell), store `to` at that port's offer address. Then if nothing was offered leave 0. Otherwise `(OFFERS)`: taken on port k: find the first debt whose port is k, write `4 (HDR) 4 !b about !b`, `(PAID)`; leave -1 either way. + - `(IDLE)`: `L:` set `(QUIET)`; if `(MQ#)` is not 0, go to `HAVE` as now; if `(OWED#)` is not 0, `(PAY) drop jump L`; else the blocking read of "any" as now. The branch that went round taking writers is deleted. + - Delete from `(GATE)`'s old body and comments: the writers loop, the `(NEAR)` comparison, `NOTYET`/`NOONE`. Delete `(PAY1)`'s `BUSY`/`BLIND`/`LEAVE`. `(NEAR)` and `NEIGHBOUR` stay for `(CENTRE)`. + - `(RAISED)`'s quiet branch is unchanged: an error 18 raised while paying counts and returns to `(IDLE)`, where `(PAY)` then drops the debt by `(THERE)`. +- [ ] **Step 4: The lone node.** In `v4/system/boot.c`'s line loop and nucleus-loading loop, and in `test_host_quit.c`'s `run_line` and `boot_with`, call `(void)v4_node_offer_as_write(n);` at the top of each pass, before `asking` is looked at. Nothing else changes there: a write to a port with no one on it is already error 18. +- [ ] **Step 5: Run** `make -C v4 run-64-test_host_mesh.c run-32-test_host_mesh.c`; then `make -C v4 -k test`, `make -C v4 -k sanitize`, `make -C v4 hosted-check`. Expected: 0 failures; `hosted-check` shows POST 538 of 538, the three ISAs identical, and the typed `5 7 PORT!` and `5 AWAIT` lines as before. `test_host_unit.c`'s "Hera blocked writing" check still passes (it now passes by the nucleus's "Interrupted", not the engine's). +- [ ] **Step 6: Mutation checks.** (a) In `(GATE)` make the console test always false: the "ends that line Interrupted" check fails. (b) In `(PAY)` offer only the first debt: "two owed" fails. Put both back. +- [ ] **Step 7: Commit** `feat(v4.0.0): every message begins through the wait -- a stuck node holds up no one` and push. + +### Task 3: What the wait replaced comes out; the documents; the boots + +**Files:** Modify `v4/include/v4/fabric.h`, `v4/src/fabric.c` (`interrupt_error`, `v4_fabric_interrupt_error`, `pending` in `v4_device`), `v4/include/v4/node.h`, `v4/src/node.c` (`since_look`, `v4_node_words_since_look`), `v4/include/v4/image.h` (`V4_ERROR_INTERRUPTED` stays only if still used), `v4/tests/test_fabric.c`, `v4/tests/test_host_mesh.c`, `v4/tests/test_host_unit.c` (device initialisers, the `v4_fabric_interrupt_error` call), `docs/v4.0.0/MESH.md`, `v4/README.md`. + +**Interfaces — Consumes:** Tasks 1 and 2. **Produces:** nothing new. + +- [ ] **Step 1: Write the failing test** in `test_host_unit.c`, in place of "Hera blocked writing to a node that is stuck": node 11 stuck; Hera's line `: T11 S" 1 DROP" 11 SEND 67 EMIT ; T11` is still running and she is `v4_node_in_wait`, not `asking` (acceptance 5); `tell(10, "11 KILL 68 EMIT")` gives "Interrupted" before "D", never "C"; node 11 is gone; `1 2 + .` gives "3 ". And one more: with node 12 stuck and owed a NACK by Hera, `tell(13, "13 100 * .")` gives "1300 ": a stuck node of the unit holds up no other. Remove the `v4_fabric_interrupt_error` call from the test's set-up. +- [ ] **Step 2: Run** `make -C v4 run-64-test_host_unit.c`. Expected: the `v4_node_in_wait` check passes already (Task 2); so take Task 2's console test out of `(GATE)` to see "Interrupted" fail, and put it back. Say so in the ledger. +- [ ] **Step 3: Remove** `interrupt_error` and its loop, `v4_fabric_interrupt_error`, `pending` from `v4_device` and every initialiser, `since_look` and `v4_node_words_since_look`, and the section of `test_fabric.c` that tested them. `grep -rn 'interrupt_error\|\.pending\|console_pending\|since_look' v4 kernel/src/v4` is empty. +- [ ] **Step 4: Run** `make -C v4 -k test`, `sanitize`, `hosted-check`, and `mkcapsule --lint capsules/` with the freshly built tool. 0 failures, lint clean. +- [ ] **Step 5: Documents.** `MESH.md`: 7b's rule and 7b.7 lose the "stuck node can hold up its neighbours" limit and point to 7c; 7b.6's "A blocked write" paragraph says it was replaced by 7c; 4.1's table gains the offer and wait addresses; step 6d in section 10 is marked done with the tests, the logs and the hash. `v4/README.md`: the last sentence of "Refusals and waits" becomes what now holds. +- [ ] **Step 6: The three boots**, in order amd64, aarch64, riscv64, each `clean qemu` with `STARFORTH_V4=1` and the typed session used for step 6c. Expected on each: `PARITY:V4_POST tests=538 pass=538 fail=0`, `PARITY:OK`, `POST: PASSED`, the same `word_count` and `dict_hash` as `hosted-check`, and the session's lines as in `logs/20261007-220949`. +- [ ] **Step 7: Commit** `feat(v4.0.0): the wait replaces looking and the console release; step 6d written up`, with the logs, and push.