Skip to main content

weft-resuming-reads

Read before writing, fixing or reviewing a node that reads a bus or a stream (a bus reader, a `Generator` input), above all one that also waits, yields or reads at more than one line, and when a node fails with `read directly (not through its state)`, `opened a second reader` or `paused with messages of a bus it had not read yet`: the one rule a read across a pause follows, `state.read`, the shapes it allows and refuses, and each error with its fix.

Source facts

Repository
WeaveMindAI/weft
Last source activity
November 3, 2026 at 07:13
Detected SKILL.md language
English
Stars
2,024
Forks
223

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
weft-resuming-reads
description
Read before writing, fixing or reviewing a node that reads a bus or a stream (a bus reader, a `Generator` input), above all one that also waits, yields or reads at more than one line, and when a node fails with `read directly (not through its state)`, `opened a second reader` or `paused with messages of a bus it had not read yet`: the one rule a read across a pause follows, `state.read`, the shapes it allows and refuses, and each error with its fix.
# Reading a bus or a stream across a pause A body that pauses runs again from its top. weft saves how far its reader got on each bus and how many items it took off each stream, and nothing about the lines it ran. It does not save what your code did with an item it already read: anything you need again after a pause goes through the state. ## The calls - `bus.cursor()?`, `cursor_from_start()?`, `cursor_at(offset)?` open your one reader on a bus; a body run again gets it back where it stopped. - A **direct read** is any read not through the state: `reader.next(&mut state)`, `next_message`, `next_json`, `next_bytes`, `gen.next(&mut state)`, `gen.try_next(&state)`, `gen.drain(&mut state)`. - `state.read(|s| &mut s.field, &mut reader).await?`: field filled, it hands that back and reads nothing; field empty, it reads the next item into the field (`Option<BusEntry>` off a bus, `Option<T>` off a `let mut rows: Generator<T>`). It saves nothing by itself (the field goes on record at the next pause, `save()`, or end of a `once` or `wait`). Set the field back to `None` when you are done with the item. - `state.once(|s| &mut s.f, async |st| { .. reader.next(st).await? .. })`: the work reads with the state as `st`. - `entry.as_json()`: a JSON message's kind and value, `None` for a join, a leave or bytes. - `gen.drain(&mut state)` can pause too: it hands the list over only at the stream's end, and on a pause weft saves the items it held for the resumed `drain` to start from. - `bus.wait_for(name, &mut state)` is not a read and the rule ignores it: it asks whether that name ever joined, the list of who joined comes back with the bus, and a body run again gets past it at once. No `once` around it. ## The rule The first time a run of the body (first run or resumed) reaches a place where it can pause (a bus read, a stream pull, `state.read`, `state.wait`, a yield, a wait between a ctx helper's looks), no direct read may have happened before it in that run, except inside a `state.once` that has finished. A filled `state.read` or `state.wait` still counts as a place; `try_next` never waits, so it is a direct read but no place. A place is its line and what it reads (the bus, the stream, or the state field); one already reached in this run (the next turn of a loop over one reader) is fine. Read directly in your node's own code, not inside a helper function, or read through the state: a helper's reads all carry the helper's line, so two phases reading through one helper look like one place. weft checks it live and saves nothing for it; while the firing holds its run (`ctx.hold()`) nothing it calls pauses or counts as a place, but its last direct read counts once the hold is dropped (`let m = { let mut h = ctx.hold(); r.next(&mut h).await? }; state.wait(..)` fails at the wait; reading and waiting inside one hold passes). If a body reads directly and then, after a pause, takes another path than before, it fails at the first place on the new path. So: before a place your run can pause, branch only on your state; anything from outside (the time, a random value, a call to a service) goes in a state.once. Without a direct read before the split nothing stops a wrong path: weft matches each wait to the record by its number in the firing, so a resumed body on another path that reaches a different wait of the same signal kind silently gets the earlier wait's answer (only a different kind fails). Allowed (`is_stop` stands for your own test; `m` is an `Option<BusEntry>`, so in real code match on `m.as_ref().and_then(BusEntry::as_json)`): ```rust // a loop reading directly loop { let m = r.next(&mut state).await?; handle(m); } // a greeting through the state, then a loop let hello = state.read(|s| &mut s.hello, &mut r).await?; loop { let m = r.next(&mut state).await?; } // two buses in one loop (pause in either read) loop { let a = state.read(|s| &mut s.a, &mut ra).await?; let b = rb.next(&mut state).await?; handle(a, b); state.a = None; } // two loops with a wait between (pause in each step) let stop = state.once(|s| &mut s.first, async |st| { loop { let m = r.next(st).await?; if m.is_stop() { return Ok(m); } } }).await?; state.wait(|s| &mut s.ok, approval).await?; loop { let m = r.next(&mut state).await?; } ``` Refused on the first run, no pause needed: ```rust let hello = r.next(&mut state).await?; loop { let m = r.next(&mut state).await?; } // fails at the loop's read let m = r.next(&mut state).await?; state.wait(|s| &mut s.ok, approval).await?; // fails at the wait loop { let a = ra.next(&mut state).await?; let b = rb.next(&mut state).await?; // fails here } for r in &mut readers { r.next(&mut state).await?; } // fails at the second reader let r1 = bus.cursor()?; let r2 = bus.cursor()?; // fails at the second cursor ``` A loop inside a `once` that is re-entered after a pause starts over at the reader's saved position, so anything it adds up across turns lives in the state. ## The errors, and what to change - `node '<id>' read directly (not through its state) at <line> before it waited at <line>, a place its run can pause` (`waited` may be `read a bus`, `pulled a stream`, `drained a stream`, `read through its state`, `yielded`, `waited between a helper's looks`): if the reads take turns in one loop (two buses, say), read the earlier one with `state.read`; if the earlier reads are a phase that ends, put that phase in a `state.once`. - `node '<id>' opened a second reader on a bus at <line>, which it already opened one on at <line>`: open the reader once, before every loop that reads the bus, and use it everywhere; to keep only some messages, match on the entry or use `cursor.with_filter(..)`. - `this node paused with messages of a bus it had not read yet, and the bus no longer holds them`: over the pause the bus lost messages this reader had not reached: an ephemeral or bytes bus comes back empty, a journaled one keeps only the JSON messages of its window that come after the newest one over 100 KB. Have the creating node keep it journaled with JSON and a wide `window`, or read with `ctx.hold()` (a held reader behind the window skips what was dropped without an error).
View on GitHub