Decide whether the block progress marker is a primitive or dead weight, and stop the host spending a module's quota without saying so.
What is true today
persist_progress_marker (crates/nexum-runtime/src/supervisor/cursors.rs:98) writes last_dispatched_block:<chain_id> after every successful block dispatch, per module and per chain.
Nothing in the runtime ever reads it back. progress_key has exactly one caller, the write. Contrast chainlog_cursor:<keccak>, which read_chain_log_cursor (cursors.rs:50) does read, and which therefore earns its place.
So on a two-second chain with ten block-subscribed modules the engine performs five redb writes a second for a value with no consumer.
Why this is more than a wasted write
Both host-written keys go through store.module(module), so they land in the module's own local-store namespace, and the namespace footprint is what the quota is measured against (host/local_store_redb.rs:169-183).
Two consequences the tree does not state anywhere.
A module's max_state_bytes is partly spent by the host, on the module's behalf, without the author being able to see or influence it.
A key an author picks can collide with a host key. last_dispatched_block:1 written by a module is the same key the host writes for chain 1.
And the failure mode is asymmetric. persist_progress_marker warns and continues, so a module at its quota silently stops updating a marker nothing reads, which is harmless. commit_chain_log_cursor warns and continues too, so the same module silently stops advancing its chain-log cursor, and on the next start it replays from a stale position. That is a real behaviour, it is quota-dependent, and it is documented nowhere.
Why now
docs/production.md:136 and ADR-0019:81 list both keys as store contents an operator may inspect. Neither says an author may read them, neither says an author must avoid them, and neither says the host spends the module's quota.
This is pre-v1 contract surface. #239 renames the whole trigger vocabulary, and these two keys are named in ADR-0019 alongside it, so settling them costs nothing now and is a breaking change later.
The question to answer
Does a module need to detect a gap? Blocks are deliberately not replayed: a log carries content that exists once, a block trigger is a clock, and replaying a clock after an outage is wrong, since a module doing periodic work wants the current head rather than five hundred historical ticks. That asymmetry is correct and should be recorded in an ADR while this is decided.
But a module that does care can only learn it missed blocks 100 to 500 if something tells it. The marker is already in its namespace, so the cheapest gap primitive is nearly free: document that a module may read last_dispatched_block:<chain_id> from its own store, and the runtime keeps writing it. The alternative is to delete the write and let a module that cares keep its own marker.
Pick one. Do not leave a write with no reader and no documented contract.
Scope
- Decide: documented gap primitive, or delete the write.
- If it stays, record the asymmetry (logs backfill, blocks do not replay) in an ADR, and document the key as author-readable.
- Either way, resolve the namespace question. Host-written keys sharing an author's keyspace needs either a reserved prefix an author cannot write, or a documented rule that these names are taken.
- Either way, state that host-written keys consume
max_state_bytes, and say what happens at the quota, including that a stalled chain-log cursor replays from a stale position on restart.
Done when
No host write to a module namespace lacks a reader or a documented contract.
An author knows which keys are theirs.
The quota interaction is written down, including the stale-cursor consequence.
Decide whether the block progress marker is a primitive or dead weight, and stop the host spending a module's quota without saying so.
What is true today
persist_progress_marker(crates/nexum-runtime/src/supervisor/cursors.rs:98) writeslast_dispatched_block:<chain_id>after every successful block dispatch, per module and per chain.Nothing in the runtime ever reads it back.
progress_keyhas exactly one caller, the write. Contrastchainlog_cursor:<keccak>, whichread_chain_log_cursor(cursors.rs:50) does read, and which therefore earns its place.So on a two-second chain with ten block-subscribed modules the engine performs five redb writes a second for a value with no consumer.
Why this is more than a wasted write
Both host-written keys go through
store.module(module), so they land in the module's own local-store namespace, and the namespace footprint is what the quota is measured against (host/local_store_redb.rs:169-183).Two consequences the tree does not state anywhere.
A module's
max_state_bytesis partly spent by the host, on the module's behalf, without the author being able to see or influence it.A key an author picks can collide with a host key.
last_dispatched_block:1written by a module is the same key the host writes for chain 1.And the failure mode is asymmetric.
persist_progress_markerwarns and continues, so a module at its quota silently stops updating a marker nothing reads, which is harmless.commit_chain_log_cursorwarns and continues too, so the same module silently stops advancing its chain-log cursor, and on the next start it replays from a stale position. That is a real behaviour, it is quota-dependent, and it is documented nowhere.Why now
docs/production.md:136and ADR-0019:81 list both keys as store contents an operator may inspect. Neither says an author may read them, neither says an author must avoid them, and neither says the host spends the module's quota.This is pre-v1 contract surface. #239 renames the whole trigger vocabulary, and these two keys are named in ADR-0019 alongside it, so settling them costs nothing now and is a breaking change later.
The question to answer
Does a module need to detect a gap? Blocks are deliberately not replayed: a log carries content that exists once, a block trigger is a clock, and replaying a clock after an outage is wrong, since a module doing periodic work wants the current head rather than five hundred historical ticks. That asymmetry is correct and should be recorded in an ADR while this is decided.
But a module that does care can only learn it missed blocks 100 to 500 if something tells it. The marker is already in its namespace, so the cheapest gap primitive is nearly free: document that a module may read
last_dispatched_block:<chain_id>from its own store, and the runtime keeps writing it. The alternative is to delete the write and let a module that cares keep its own marker.Pick one. Do not leave a write with no reader and no documented contract.
Scope
max_state_bytes, and say what happens at the quota, including that a stalled chain-log cursor replays from a stale position on restart.Done when
No host write to a module namespace lacks a reader or a documented contract.
An author knows which keys are theirs.
The quota interaction is written down, including the stale-cursor consequence.