Documentation
¶
Overview ¶
Package events is the card event stream of nova-tools #2563 and the SQLite fold that is its record: every card transition XADDs one entry to `cards:done`, and one consumer folds the stream into a SQLite file whose views answer "how many, how much, per model, per route, per bench, per day".
THERE IS ONE STREAM (Rowan's ruling on the bus 2026-09-22, accepted by Johnny 17:16Z). `cards:done` is the stream internal/record and internal/ci already read; this package does not mint a second one beside it, it adds the event fields to the entries of that stream. The SQLite file is a fold of `cards:done` and nothing else: a view that `fold --rebuild` can recompute from the stream at any moment, never a second source of truth. An entry written before the fields were added carries no `event` field; the fold counts it as skipped rather than guessing it into `ok` or `fail`.
The division of labour is Johnny's (reports/redis-for-nova-tools-2026-09-21.md section 8, adopted): Redis holds ids, counts and event ids and NOTHING else -- never a diff, a test, a prompt, a transcript or a disposition -- and every query lives in the fold, over core Redis types only, so Valkey stays a drop-in and the hot store never becomes the database. Validate enforces that literally: a field over maxFieldBytes or carrying a control character is refused at the door rather than trusted to the caller's good manners.
Streams deliver at least once, so the event id is the primary key of every fold table and a redelivery is a no-op. That is what makes Johnny's bar 3 -- a hundred DONEs, kill the fold, restart, the count is a hundred -- a property of the schema rather than of luck.
Index ¶
- Constants
- Variables
- func EmitOne(in EmitInput) int
- func Float64(f float64) *float64
- func FoldRun(in FoldInput) int
- func Int64(n int64) *int64
- func KindList() string
- func SortedIDs(entries []Entry) []string
- func Text(s string) string
- type DB
- func (d *DB) Apply(ctx context.Context, entries []Entry) (inserted, skipped int, _ error)
- func (d *DB) Close() error
- func (d *DB) Count(ctx context.Context) (int, error)
- func (d *DB) CountKind(ctx context.Context, k Kind) (int, error)
- func (d *DB) Dump(ctx context.Context, w io.Writer) error
- func (d *DB) Path() string
- func (d *DB) Report(ctx context.Context, w io.Writer, max int) error
- type Decision
- type Dial
- type EmitInput
- type Emitter
- type Entry
- type Event
- type FakeStream
- func (f *FakeStream) Ack(ctx context.Context, group string, ids ...string) error
- func (f *FakeStream) Close() error
- func (f *FakeStream) Emit(ctx context.Context, e Event) (string, error)
- func (f *FakeStream) EnsureGroup(ctx context.Context, group string) error
- func (f *FakeStream) Len() int
- func (f *FakeStream) Next(ctx context.Context, group, consumer string, count int, block time.Duration) ([]Entry, error)
- func (f *FakeStream) Pending(ctx context.Context, group, consumer string, count int) ([]Entry, error)
- func (f *FakeStream) PendingCount(group string) int
- func (f *FakeStream) Range(ctx context.Context, start string, count int) ([]Entry, error)
- func (f *FakeStream) StreamName() string
- type FoldInput
- type Folder
- type Kind
- type Reader
- type RedisStore
- func (s *RedisStore) Ack(ctx context.Context, group string, ids ...string) error
- func (s *RedisStore) Close() error
- func (s *RedisStore) Emit(ctx context.Context, e Event) (string, error)
- func (s *RedisStore) EnsureGroup(ctx context.Context, group string) error
- func (s *RedisStore) Next(ctx context.Context, group, consumer string, count int, block time.Duration) ([]Entry, error)
- func (s *RedisStore) Pending(ctx context.Context, group, consumer string, count int) ([]Entry, error)
- func (s *RedisStore) Range(ctx context.Context, start string, count int) ([]Entry, error)
- func (s *RedisStore) Reach(ctx context.Context) error
- func (s *RedisStore) SetClock(now func() time.Time)
- func (s *RedisStore) StreamName() string
- type Stats
- type Store
- type Writer
- type WriterOptions
Constants ¶
const ( Stream = "cards:done" Group = "fold" MaxLen = 1_000_000 )
Stream is the one stream every card transition writes to, Group the consumer group the fold reads under, and MaxLen the approximate cap XADD trims to (MAXLEN ~ 1e6): a stream that grows without a bound is a store that fills a disk no one is watching.
const ( DefaultPasswordEnv = "NOVA_REDIS_BENCH_PASSWORD" AddrEnv = "NOVA_REDIS_ADDR" HostEnv = "NOVA_REDIS_HOST" PortEnv = "NOVA_REDIS_PORT" )
The environment this writer reads. The password variable is the one `nova-secrets exec --only NOVA_REDIS_BENCH_PASSWORD` fills; the address is configuration, never a constant in this repository.
const DefaultUser = "bench"
DefaultUser is the fleet's ACL user for the card path: it needs XADD on `cards:done`, the one stream (Stream), and nothing else. The fold reads under its own user.
const SchemaVersion = 3
SchemaVersion is the migration version schema.sql installs. Version 2 added the decisions table (#2623), the record the decide_log table held. Version 3 (#3159) rebuilds every view on open and prints $/landed as a lower bound with its coverage.
const Tables = "attempts,reads,landings,decisions"
Tables is the fold's tables, as the --init receipt names them.
Variables ¶
var Kinds = []Kind{Queued, Leased, Started, Turn, OK, Fail, Asked, Harvested, PullReq, Read, Landed, Jev, Decide}
Kinds is every kind the stream accepts, in banner order.
Functions ¶
func EmitOne ¶
EmitOne validates the event, writes it and prints the receipt. Exit 0 wrote one entry, exit 2 could not run.
func Int64 ¶
Int64 and Float64 are the one-line way to fill a reported number: Event{USD: Float64(0.11)}.
func KindList ¶
func KindList() string
KindList is the kinds as the refusal and the usage banner spell them.
func Text ¶
Text makes a free-text value fit for the stream: escaped to one line and capped at the field ceiling with oneline's byte mark, so a long reason arrives cut AND SAYS SO rather than being refused at the door and losing the whole decision. The uncut text is in the JSON lines log the decision was also written to.
Types ¶
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is the fold's SQLite file: four tables keyed on the event id and the views over them.
func OpenDB ¶
OpenDB opens (creating if absent) the file at path and applies the schema. The schema is idempotent, so this runs on every start.
func (*DB) Apply ¶
Apply folds one batch of entries in ONE transaction and reports how many rows were new. The insert is ON CONFLICT DO NOTHING on the event id, so a redelivery adds nothing; a batch either lands whole or not at all, and the caller acks only after it lands.
An entry the stream holds that this package cannot read is a skip, not a failure: a writer with a bug must not stop the fold for every other writer. It is counted and named.
func (*DB) Count ¶
Count is how many rows the four tables hold together: the number Johnny's bar 3 asks for.
func (*DB) Dump ¶
Dump writes every row of the three card tables as TSV, table by table and ordered inside each table by event id, then the decisions under a header of their own columns. It is deterministic by construction -- no fold timestamp, no insertion order -- which is what lets a rebuild be compared with an incremental fold byte for byte.
type Decision ¶
type Decision struct {
UnitID string
Kind string
Files int
Packages int
Lanes int
Lane string
RungTried string
Height int
Confidence *float64
Floor *float64
SteppedUp bool
Escalated bool
Designated bool
Source string
RowanPick string
Reason string
Wait string
AwaitingTermination bool
Refusal string
Outcome string
RungSucceeded string
Calls int
UsageFailed bool
}
Decision is the decide_log row as stream fields. Every string is an id, a name or a short capped reason; every number is a count, a height or a confidence.
type Dial ¶
Dial options. The password never arrives as a flag or an argument: the caller reads it from the environment `nova-secrets exec` put it in, and nothing here ever prints it.
type EmitInput ¶
type EmitInput struct {
Addr string
Username string
Password string // read from the environment by the caller; never a flag, never printed
Stream string
Event Event
Print bool // render the entry and write nothing, so a writer can be watched offline
Now time.Time
Timeout time.Duration
Stdout io.Writer
Stderr io.Writer
}
EmitInput is `nova-pulse event`: one XADD, one line, for the bash writers -- the launcher, harvest and the lander -- until they are Go.
type Emitter ¶
Emitter is the write half: one XADD per card transition. It is an interface so the unit tests run against the in-memory FakeStream and the integration test against the fleet store, over the same code.
type Entry ¶
Entry is one delivered stream entry: the id Redis minted and the fields the writer wrote. The id is the fold's primary key, so it is the whole of the idempotency story.
type Event ¶
type Event struct {
Label string
Attempt int
Bench string
Model string
Route string
Kind Kind
TokensIn *int64
TokensOut *int64
USD *float64
PR string
Head string
At time.Time
// Decision is the decide_log row a `decide` entry carries, and nil on every other kind.
Decision *Decision
}
Event is one card transition. Every string field is an id or a name; every number is a count or a price. There is no field for prose and that is the point.
TokensIn, TokensOut and USD are pointers because an absent cost is not a zero cost (no evidence is not negative evidence): nil means nobody reported the number, the entry then carries no such field at all, and the fold stores NULL. A zero is a writer saying zero.
func FromFields ¶
FromFields reads one stream entry back. A missing tokens_in, tokens_out or usd stays ABSENT (nil), because a writer that did not know the cost has not reported a zero cost; a number that is present and unreadable is an error, because that is a writer with a bug.
func (Event) Day ¶
Day is the UTC date the event belongs to, or "" when it carries no stamp. It is computed once at fold time so the per-day view is a column and not a string function in a query.
func (Event) Fields ¶
Fields is the entry as Redis stores it: every value a string, every name the one #2563 spells. tokens_in, tokens_out and usd are present only when the event carries them.
func (Event) Stamp ¶
Stamp returns the event with At filled from now when the caller left it zero, so the emitter stamps exactly once and a replay never restamps.
type FakeStream ¶
type FakeStream struct {
// Now is the clock Emit stamps an unstamped event with; a test may pin it.
Now func() time.Time
// FailEmit, when set, is the error every Emit returns instead of writing. It is
// the ONLY way to prove the contract the card-path writers are built on -- that a
// store which refuses an entry does not fail the card, the harvest or the landing
// -- so the fake carries a way to be broken on purpose (nova-tools #2563 item 1).
// It fails Emit alone: a fold reading a stream whose writer is broken is a
// different test.
FailEmit error
// contains filtered or unexported fields
}
FakeStream is the in-memory stream the unit tests run against: the same Store the Redis path implements, with the same group semantics -- at-least-once delivery, a pending list that survives a "crash", and an ack that is the only thing that retires an entry.
It is STRICT LIKE THE REAL TOOL (AGENTS.md): Emit validates exactly as RedisStore.Emit does, and a read under a group nobody created fails with NOGROUP, the way Redis does. A lenient fake here would ship the fold broken on the one store that matters.
func NewFakeStream ¶
func NewFakeStream() *FakeStream
NewFakeStream returns an empty stream named for the real one.
func (*FakeStream) Close ¶
func (f *FakeStream) Close() error
Close is the Store contract; the fake holds nothing to release.
func (*FakeStream) EnsureGroup ¶
func (f *FakeStream) EnsureGroup(ctx context.Context, group string) error
EnsureGroup creates the group at entry 0. A second call is not an error.
func (*FakeStream) Next ¶
func (f *FakeStream) Next(ctx context.Context, group, consumer string, count int, block time.Duration) ([]Entry, error)
Next takes up to count entries the group has never been delivered. block is accepted and ignored: the fake never waits, because a test that waits on a clock is a test that fails on a loaded bench (AGENTS.md rule 3).
func (*FakeStream) Pending ¶
func (f *FakeStream) Pending(ctx context.Context, group, consumer string, count int) ([]Entry, error)
Pending returns the group's unacked entries, oldest first: XAUTOCLAIM with MinIdle 0, so a restarted fold under any consumer name takes them back.
func (*FakeStream) PendingCount ¶
func (f *FakeStream) PendingCount(group string) int
PendingCount is how many entries the group still owes an ack for.
func (*FakeStream) StreamName ¶
func (f *FakeStream) StreamName() string
StreamName is the stream this fake stands for.
type FoldInput ¶
type FoldInput struct {
Addr string
Username string
Password string
Stream string
Group string
Consumer string
DB string
Interval time.Duration
Count int
Timeout time.Duration
Max int
Once bool // one pass, then exit; the CI-shaped form
Rebuild bool // replay the whole stream into a file that does not exist yet
Init bool // apply the schema and exit, with no store
Report bool // print the views of an existing file, with no store
Dump bool // print every row, deterministically, with no store
Stdout io.Writer
Stderr io.Writer
}
FoldInput is `nova-pulse fold`: the stream's one consumer, its replay and its two offline readers (--init and --report).
type Folder ¶
type Folder struct {
Reader Reader
DB *DB
Group string
Consumer string
Count int // entries per read
Block time.Duration // how long one read may wait for a new entry
}
Folder is one consumer of the stream into one file.
func (*Folder) Once ¶
Once is one pass: reclaim what a dead predecessor left unacked, then take new entries. The order matters -- pending first -- because otherwise a fold that is killed and restarted walks past its own unfinished work and the count is short.
func (*Folder) Run ¶
func (f *Folder) Run(ctx context.Context, interval time.Duration, out io.Writer, stream string) (Stats, error)
Run holds the loop: one pass per interval until the context is cancelled. A cancelled context is the shutdown, not a failure, so it returns the running total and no error -- which is what makes "kill it half way and restart" an ordinary thing to do.
type Kind ¶
type Kind string
Kind is the transition one entry records.
const ( Queued Kind = "queued" Leased Kind = "leased" Started Kind = "started" Turn Kind = "turn" OK Kind = "ok" Fail Kind = "fail" Asked Kind = "asked" Harvested Kind = "harvested" PullReq Kind = "pr" Read Kind = "read" Landed Kind = "landed" Jev Kind = "jev" )
The kinds, in the order a card walks them. `read` is a friend's read of a pull request, `landed` the lander's merge, `jev` a Jev decision; the rest are the card's own life. `decide` (decide.go) is a routing decision carrying the decide_log fields: the record that was the decide_log table until #2623.
const Decide Kind = "decide"
Decide is a routing decision: one entry per verdict, carrying a Decision.
type Reader ¶
type Reader interface {
EnsureGroup(ctx context.Context, group string) error
Pending(ctx context.Context, group, consumer string, count int) ([]Entry, error)
Next(ctx context.Context, group, consumer string, count int, block time.Duration) ([]Entry, error)
Ack(ctx context.Context, group string, ids ...string) error
Range(ctx context.Context, start string, count int) ([]Entry, error)
}
Reader is the read half the fold uses. It is XREADGROUP, XAUTOCLAIM, XACK and XRANGE and nothing else: core types only, no server-side logic (Johnny, section 8).
- EnsureGroup makes the stream and the group at entry 0, so a fold started after the first writer still reads the whole stream.
- Pending claims what this group was delivered and never acked, from any consumer, so a fold that is killed and restarted under a new consumer name loses nothing.
- Next takes new entries.
- Ack is the commit, and it happens only after the fold's transaction has committed.
- Range is the replay --rebuild reads, and it touches no group state.
type RedisStore ¶
type RedisStore struct {
// contains filtered or unexported fields
}
RedisStore is the live stream on the fleet Redis. go-redis is already this repository's Redis client (internal/redisq, internal/record, internal/ci), so this adds no dependency; the five calls below are XADD, XREADGROUP, XAUTOCLAIM, XACK and XRANGE, which is the whole of what "core types only" allows.
func Open ¶
func Open(ctx context.Context, d Dial) (*RedisStore, error)
Open names the store and sends nothing (#3277): the first read or write dials, so a bad address fails the caller's first batch instead of every open paying a PING round trip.
func (*RedisStore) Ack ¶
Ack commits the delivery. It runs after the fold's own transaction, never before.
func (*RedisStore) Emit ¶
Emit is the one XADD: `XADD <stream> MAXLEN ~ 1000000 * <fields>`. The event is validated first, so nothing but ids and counts can ever reach the store.
func (*RedisStore) EnsureGroup ¶
func (s *RedisStore) EnsureGroup(ctx context.Context, group string) error
EnsureGroup creates the group at entry 0, making the stream if it is absent. A group that already exists is not an error: every fold start runs this.
func (*RedisStore) Next ¶
func (s *RedisStore) Next(ctx context.Context, group, consumer string, count int, block time.Duration) ([]Entry, error)
Next takes up to count new entries, blocking for at most block. Zero block is never passed to Redis, which reads BLOCK 0 as "forever".
func (*RedisStore) Pending ¶
func (s *RedisStore) Pending(ctx context.Context, group, consumer string, count int) ([]Entry, error)
Pending claims every entry this group holds unacked, from any consumer, oldest first. MinIdle is zero on purpose: the fold is a single consumer and a restart must take its own dead predecessor's work back at once, not after an idle timeout.
func (*RedisStore) Range ¶
Range reads the stream itself from start, forwards, touching no group. It is the replay `fold --rebuild` walks.
func (*RedisStore) Reach ¶
func (s *RedisStore) Reach(ctx context.Context) error
Reach sends one PING, for a caller that must refuse before costly work rather than find the store down after it; Open itself sends nothing (#3277).
func (*RedisStore) SetClock ¶
func (s *RedisStore) SetClock(now func() time.Time)
SetClock installs the clock Emit stamps an unstamped event with.
func (*RedisStore) StreamName ¶
func (s *RedisStore) StreamName() string
StreamName is the stream this store reads and writes.
type Stats ¶
type Stats struct {
Read int // entries taken from the stream
Inserted int // rows the fold did not already hold
Acked int // deliveries committed back to the group
Skipped int // entries this package could not read
}
Stats is what one fold pass did.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer holds an open store, or nothing at all. A disabled writer is not an error: it is the shape every bench without the store's password runs in.
func OpenWriter ¶
func OpenWriter(ctx context.Context, opt WriterOptions) *Writer
OpenWriter resolves the configuration and dials, and NEVER returns an error: a writer it could not open is a writer that emits nothing. A dial that failed says so once, here, rather than once per card.
func (*Writer) Close ¶
func (w *Writer) Close()
Close releases the store. It is safe on a nil or disabled writer.
func (*Writer) Enabled ¶
Enabled reports whether an entry would actually be written. It exists for a receipt line, never as a guard a caller has to remember: Send is safe either way.
func (*Writer) Send ¶
Send writes one entry and returns NOTHING. A refused entry, a store that is down and a context that was cancelled are all one line on the log and a return: the card, the harvest or the landing that called this is already done and is not undone by a measurement. There is no error to ignore, because there is no error to return.
func (*Writer) StreamName ¶
StreamName is the stream entries land on, for a receipt.
type WriterOptions ¶
type WriterOptions struct {
Addr string // --events-store; "" resolves from the environment
Username string // "" is DefaultUser
PasswordEnv string // "" is DefaultPasswordEnv
Stream string // "" is Stream
Timeout time.Duration
Log io.Writer // where the one line per failure goes; nil discards it
// Lookup is the environment. nil is os.Getenv, and a test passes a map.
Lookup func(string) string
// Dial opens the store. nil is the Redis one, and a test passes a fake -- including
// a fake that ERRORS, which is the only way to prove the caller survives it.
Dial func(ctx context.Context, d Dial) (Store, error)
}
WriterOptions is how a verb turns the stream on. Every seam a test needs is here, so no test of a card path opens a connection (AGENTS.md rule 2).