Documentation
¶
Overview ¶
Package wake is nova-wake's machinery: the state file that makes a change a change, the three sources it polls, the view over a bus checkout's commits that says whether a line has gone silent, and the serve loop that wakes a harness from outside a session. deprecated/docs/SPEC-WAKE.md is normative for all of it.
The whole design is one sentence: poll, compute a state value, compare it to the stored one BYTE FOR BYTE, and a difference is a change. Nothing here decides whether a change is important -- a window asked to be woken on a change, and importance is the window's to judge.
This file is the state, and it carries the second lesson of 2026-09-11 in its codec. One watched entry was stored as a tab-joined string whose last field was often empty; the reload dropped the trailing empty field, so the reloaded value never equalled the freshly computed one, and the watcher woke the window every interval, forever, with nothing to say. A false wake is worse than a missed one, because the window learns to stop reading. So: the separator is not a character a reader can eat, every field is escaped before it is joined, and Compose and Decompose are inverse over every value including the empty one.
Index ¶
- Constants
- Variables
- func AcceptBus(tool, found string) bool
- func BusCarriedEntries(out string) int
- func BusNoteIDs(out string) []string
- func BusQueued(st *State) int
- func BusRefusal(tool, found string) string
- func BusVersion(ctx context.Context, timeout time.Duration) (string, error)
- func CapKind(kind string) string
- func ClearAdvance(st *State)
- func Compose(parts ...string) string
- func Decompose(s string) []string
- func DeliveryID(key, value string) string
- func Dur(d time.Duration) string
- func DurShort(d time.Duration) string
- func FormatSlotLease(l *SlotLease) string
- func Head(ctx context.Context, dir string, timeout time.Duration) (sha, at string)
- func HereLine(now time.Time, b Bench, quietLoad string) string
- func Interrupted(st *State) bool
- func IsFinal(value string) bool
- func IsFinalRun(value string) bool
- func IsForbiddenShell(name string) bool
- func IsForbiddenSwarmVerb(arg string) bool
- func LineIsOffline(value string) bool
- func LockAdvance(busDir, as string) (release func(), holder string, err error)
- func LockName(path string) string
- func LockState(path string) (release func(), holder string, err error)
- func NewOrModified(value string, had bool) string
- func ProbeScope(busDir, remote, branch, as, line string) string
- func ReadIDs(path string) (map[string]bool, error)
- func ReadRest(path string) (map[string]Rest, error)
- func Render(kind, key, value string, now time.Time) string
- func ReportIdentity(value string) string
- func SplitEntry(name string) (repo, number string, err error)
- func SplitHead(name string) (repo, sha string, err error)
- func SplitPR(name string) (repo, number string, err error)
- func SplitRef(name string) (repo, branch string, err error)
- func Stamp(t time.Time) string
- func StampOf(s string) string
- func TempName(path string) string
- func Unreadable(value string) (bool, string)
- func ValidateDaemonArgv(argv []string, targetAs string) error
- func ValidateNotePath(notePath string) error
- func ValidateNotePaths(notePaths []string) error
- func WithCommit(value, head string) string
- func WriteSlotLeaseAtomic(leasePath string, lease *SlotLease) error
- type Advancer
- type Bench
- type Branches
- type Bus
- type Clock
- type Daemon
- type DaemonConfig
- type Entries
- type EvGithub
- type Event
- type Fake
- type Item
- type LaneRead
- type LaneResult
- type Lines
- type Locks
- type PRs
- type Probe
- type Real
- type Record
- type Reports
- type Resp3ConnPool
- func (p *Resp3ConnPool) Close() error
- func (p *Resp3ConnPool) Heartbeat(ctx context.Context) error
- func (p *Resp3ConnPool) Run(ctx context.Context, every time.Duration, onWake func(redis.XMessage)) error
- func (p *Resp3ConnPool) Subscribe(ctx context.Context) error
- func (p *Resp3ConnPool) Wait(ctx context.Context, block time.Duration) ([]redis.XMessage, error)
- type Rest
- type Result
- type Runs
- type SlotLease
- type SlotLeaseConfig
- type SlotLeaseHeldError
- type SlotLeaseManager
- type Source
- type State
- func (s *State) ClearFail(source string)
- func (s *State) Cold() bool
- func (s *State) Delete(key string)
- func (s *State) Evict()
- func (s *State) Fail(source, reason string, now time.Time) (n int, since, last string)
- func (s *State) Get(key string) (string, bool)
- func (s *State) IsPending(key string) bool
- func (s *State) Keys() []string
- func (s *State) MarkPrinted(r Record)
- func (s *State) Newest(key string) (string, bool)
- func (s *State) Observe(key, value, id string) bool
- func (s *State) ObserveDisplay(key, value, display, id string) bool
- func (s *State) Pending() int
- func (s *State) PrintedID(key string) string
- func (s *State) Queue() []Record
- func (s *State) RecordOnly(key, value string)
- func (s *State) Save(path string) error
- func (s *State) Set(key, value string)
- func (s *State) Sight(key string)
- func (s *State) Streak(source string) (n int, since, last string)
Constants ¶
const ( // HeaderCap is the depth one header is read to looking for the blank line // that ends it. IT IS A FIXED CAP, NOT A FLAG AND NOT A SHARE OF // --correlate-bytes (draft 9, K4a): it does not move when the byte budget // moves and no option in this spec raises it, so a header that does not end // inside it is unreadable by this probe AT EVERY BUDGET. HeaderCap = 4 << 10 // DefaultCorrelateMax is --correlate-max's default: the bound State already // uses for its sighting memory. DefaultCorrelateMax = 300 // DefaultCorrelateBytes is --correlate-bytes's default. DefaultCorrelateBytes = 262144 // GitWall is the wall clock on ONE git process this read starts. The // two-minute law applied to a read the same way --gh-timeout applies it to // the forge -- and it is a real deadline on a real process: the context // kills it, and the pipe it writes to carries a read deadline of its own, so // a wedged git cannot hold a poll open with a child still running. GitWall = 10 * time.Second // WholeReadCap is the ceiling on the whole read: --interval or 30s, // whichever is smaller, and 30s where no --interval was given. WholeReadCap = 30 * time.Second // GapCap bounds the retained gap list: a bound on a read that becomes an // unbounded bound on the state is not a bound. GapCap = 64 // ReceiptsFile is the lane's append-only receipt file (SPEC.md, nova-bus -- // The receipt rule). ReceiptsFile = "RECEIPTS" )
const ( // GapHeaderCap: no blank line ends this header inside the fixed 4 KiB. It // is PERMANENT AT EVERY BUDGET and no --correlate-bytes raise clears it. GapHeaderCap = "header-cap" // GapHeader: the header ended inside the cap and the bus's own semantics // could not parse it. No budget change clears that either; the fix is where // the note was written. GapHeader = "header" // GapBudget: the item's whole cost is larger than the whole of // --correlate-bytes, so no poll AT THIS BUDGET can take it. It stands only // while the budget stands. GapBudget = "budget" // GapObject: an object this read could not get from git at all. Restoring // it resolves the gap and no budget change does. GapObject = "object" // GapCommit: a commit whose own listing could not be read, so the items it // added are unknown rather than absent. GapCommit = "commit" )
The gap kinds. WHICH REMEDY APPLIES DEPENDS ON WHICH KIND IT IS, AND THE LINE NAMES IT: a caller must never be told to raise a budget against a cap that is not a budget, and must never be told a cap is fixed where a raise would in fact read the item.
const ( StatePresent = "PRESENT" StateSilent = "SILENT" StatePinged = "PINGED" StateAnswered = "ANSWERED" StateResting = "RESTING" StateUnreconciled = "UNRECONCILED" )
The seven state words.
const ( DefaultSilentAfter = 5 * time.Minute DefaultAnswerWithin = 2 * time.Minute )
The two durations Glenn's five-minute availability rule of 2026-09-12 gives the family. A duration gets a default here only when a different number per window would be a bug, and two windows disagreeing about when a friend is silent -- or about how long to wait for the answer before reassigning their work -- is exactly that bug.
const ( DefaultSlotLeaseTTL = 15 * time.Second DefaultSlotLeaseHeartbeat = 5 * time.Second SlotLeaseFileName = ".lease" )
Default constants for slot leasing in wake. 15s TTL and 5s heartbeat loop ensure responsive liveness and fast recovery while remaining robust under bench load.
const ( KindBus = "bus" KindBusLine = "busline" KindEntry = "entry" KindReport = "report" KindLine = "line" // The amendment of 2026-09-13's four, which with line make EIGHT KINDS AND // EIGHT CAPS -- the arithmetic rule 5 recomputes as 8 * --max-lines + 17. KindPR = "pr" KindRun = "run" KindBranch = "branch" KindLock = "lock" )
Kinds are the cap's kinds. The cap is PER KIND because a flat cap over a concatenated stream means the loud kind eats the quiet one, and the quiet one is the finding the window did not already know about: forty churning entries must not hide one note.
const AdvanceMarker = "bus:advance"
AdvanceMarker is the state key that says an advance was in flight. It is written BEFORE the advance and cleared AFTER its output is durable, so a kill between the two leaves the marker for the next call to recover through.
const DefaultOfflineAfter = 10 * time.Minute
DefaultOfflineAfter is the one duration here with a default, because it is the family's rule and not a fact about one window: every line must agree about it, and it is the same number as nova-board --stale. An interval, by contrast, is a fact about one window's round trip, which is why --interval and --entry-interval have no defaults at all.
const EntryBatch = 8
EntryBatch is how many gh calls may be outstanding at once. Entries are polled in batches with one gh process per entry per poll and no pagination.
const EvGithubStream = gheventwire.Stream
EvGithubStream is the stream name, the one ghevent writes.
const ForgeBatch = EntryBatch
ForgeBatch is how many gh calls may be outstanding at once, for the sources that poll several things on one tick. It is EntryBatch's 8, because the pool the batch spends is the same pool.
const ForgeIntervalFloor = 30 * time.Second
ForgeIntervalFloor is --forge-interval's floor. It is higher than --interval's because these sources have no run length to pace by, they are one API call per watched thing per tick against a shared hourly limit, and people write comments and push branches at a rate a 30-second tick already over-serves.
const LRUMax = 300
LRUMax is the ceiling on the two parts of the state that grow with things that HAPPEN rather than with things being watched: the bus:line: sighting memory and the delivered bus:note: marks. Three hundred entries, least recently seen evicted first, and an evicted entry is a change again -- which is the safe direction to be wrong in. The prototype deleted ALL of them once the count passed 300, which turns every standing error back into a change at once: a thundering false wake at exactly the moment the bus is noisiest.
A key a queue record names is exempt whatever its age. Pending is unbounded in the state and bounded in the output, never the other way round.
const OwnedCap = 20
OwnedCap is the listing law over one repository's owned pull requests.
const PRNodeWindow = 10
PRNodeWindow is the ten-node read. Ten is the window, and it is a cap on the READ rather than on the news: past it the tool wakes rather than guesses, because a missed wake here is a reviewer's HOLD unread.
const ReportName = "RESULT.md"
ReportName is the one file name this source watches, at any depth under each --reports directory. One agreed name, because a watcher that woke on every file another line touched would wake on its own scratch.
const RunCheckLimit = 100
RunCheckLimit is the page this source does not turn. A head with more than 100 check runs is unreadable, which is VISIBLE, rather than a count that quietly stops at a page.
const RunIntervalFloor = 30 * time.Second
RunIntervalFloor is --entry-interval's floor when any --run is given. Two REST calls per head every 5 seconds is 1,440 calls an hour per head, so four watched heads spent a whole REST pool.
const WakeBrokenNoteNotFound = "WAKE BROKEN reason=note-file-not-found"
WakeBrokenNoteNotFound is the prefix used when a note file cannot be found or validated. Under Johnny's boundary: "A missing file is WAKE BROKEN, not a blind inbox. The packet names the note, not a command."
Variables ¶
var AdvanceKillPoint string
AdvanceKillPoint is the injected kill of test 11: the residual race is a kill between `nova-bus inbox --advance` RETURNING and the write of its output, and a test cannot SIGKILL a function it is calling. It is never set outside a test.
var ErrRecoveryPending = errors.New("a bus recovery is unresolved; nothing advances until the carried list is reached")
ErrRecoveryPending is what Advance answers when the state still holds an unresolved recovery. It is not a failed poll and must never reach the streak: the tool is doing exactly what step 4 says, and the caller says so in a note.
Functions ¶
func AcceptBus ¶ added in v0.13.0
AcceptBus answers whether a nova-wake built at tool may run against a nova-bus that reports found. Both arguments are passed in rather than read, so the rule is a pure function of them.
func BusCarriedEntries ¶ added in v0.14.0
BusCarriedEntries is how many carried entries one listing HELD, counted off the transcript and off nothing this tool remembers.
It exists because the recovery's completeness test compares what the `--open` read listed against `carrying=`, and every measure taken from what the classifier RELAYED is a partial count of that: a note already printed is suppressed, a line already standing is not an item, and the INBOX NOTE ids are only the notes. On 2026-09-12 the audit of packet 1 (#164, F2) found the consequence with an ordinary carried list -- one printed note and one receipt -- where every partial count was short of `carrying=`, so the recovery could never call itself complete and the marker was kept for ever. The transcript is the one thing that holds them all, so the count is taken there.
func BusNoteIDs ¶
BusNoteIDs is every INBOX NOTE id in a transcript, in the order the bus listed them. It is the BUS ORDER a caller needs for notes the classifier suppresses -- a note already handed to the receiver is suppressed and still has a place in the order the queue is dispatched in.
func BusQueued ¶ added in v0.12.0
BusQueued reports whether any queue record names a bus note. The cursor never moves while one does: mail consumed is mail spooled, mail spooled is mail printed, and a cap that elides a note defers the fetch rather than losing it.
func BusRefusal ¶ added in v0.13.0
BusRefusal is the one refusal sentence, with the versions in it. The wording is the spec's and is unchanged; only the pinned version is now this build's own rather than a literal.
func BusVersion ¶
BusVersion runs `nova-bus version` and reads the second token of its first line. It is run ONCE, before the opening line.
func CapKind ¶
CapKind maps an item kind to the kind named on its WAKE MORE line. A relayed bus line and a relayed note are both bus lines to the cap, because they are both what the bus said.
func ClearAdvance ¶ added in v0.12.0
func ClearAdvance(st *State)
ClearAdvance is the last step of the transaction: the marker is cleared only after the advance's output has been written to the state.
func Compose ¶
Compose joins fields into one line that round-trips. Each field is escaped -- "%" first as %25, then "|" as %7C, then the two characters that would end a line in the file -- and the fields are joined with "|". So a value holding a literal %7C comes back as those three characters and never as a pipe.
The separator is deliberately not a character the reader can eat: the failure this codec exists to close is a tab-joined value whose trailing empty field vanished on reload. Compose("a", "") and Compose("a") are different strings, and Decompose tells them apart.
func Decompose ¶
Decompose is Compose's inverse. strings.Split keeps every field including the empty ones, which is the half the tab-joined prototype lost.
func DeliveryID ¶
DeliveryID is the id of a value the window was shown: a note's own id for a bus note, and for every other key the first twelve hex characters of SHA-256 over <key>\x00<value>.
func Dur ¶
Dur is the one duration spelling this tool prints: Go's own, which is unambiguous and parses back with time.ParseDuration.
func DurShort ¶
DurShort is Dur with the zero tail trimmed, so the family's five minutes print as the five minutes they are named as rather than as 5m0s.
func FormatSlotLease ¶
FormatSlotLease formats the lease into key=value lines.
func Head ¶
Head reads the bus checkout's newest commit and its commit stamp, read-only, under the timeout. It is the freshness the WAKE SOURCE bus line shows, so a reader of the transcript can see a checkout stand still.
func HereLine ¶
HereLine is the one line --here prints. A platform with neither reading prints load=- or procs=-, which is A READING RATHER THAN A REFUSAL.
func Interrupted ¶ added in v0.12.0
Interrupted reports whether an advance was in flight when the last call died.
func IsFinal ¶
IsFinal is --final-only's predicate, a pure function over the state value.
An entry is FINAL when it is no longer OPEN -- MERGED or CLOSED -- or when pending=0 and it has at least one check. Green and red are both final, because the window's next action differs between them and is needed in both cases, and a watcher that woke only on green would sleep through every failure. An entry with NO checks at all is never final by the count rule: pending=0 pass=0 fail=0 is the state of an entry whose checks have not been created yet, and calling that green is the one arithmetic mistake here that would merge something red.
func IsFinalRun ¶
IsFinalRun is --final-only's predicate for a head: the entry definition with one clause fewer. A head with NO checks at all is never final, for the reason an entry with none is not -- pending=0 pass=0 fail=0 is the state of checks that have not been created yet, and calling that green is the one arithmetic mistake here that would merge something red.
func IsForbiddenShell ¶
IsForbiddenShell reports whether a binary name represents a shell.
func IsForbiddenSwarmVerb ¶
IsForbiddenSwarmVerb reports whether an argument represents a forbidden swarm verb.
func LineIsOffline ¶
LineIsOffline reads a --line state value's state field. It is here, beside the value's other readers, so that the cold-start rule asks one function rather than matching a substring of a composed value.
func LockAdvance ¶ added in v0.12.0
LockAdvance takes the exclusive lock named by (bus, as) for the whole call -- not per poll, unlike nova-bus wait, because the thing being protected is a cursor this run is MOVING rather than a checkout it is reading. A second watcher over the same pair is refused, naming the holder.
The lock lives beside the bus it names, which is a directory the caller gave: nothing here guesses a path, and nothing here writes to /tmp (rule 4).
func LockName ¶
LockName is the lock file beside the state, holding the pid of the run that took it (rule 4).
func LockState ¶
LockState takes the exclusive lock on <state>.lock for the whole call. The second run over one path is refused, and the refusal names the holder's pid and the path.
func NewOrModified ¶
func ProbeScope ¶
ProbeScope binds a probe record to its transport.
A NAME IS NOT A TRANSPORT (draft 6, K6). A state file copied to a second checkout, pointed at a second remote or branch, or run under a second caller would otherwise present another transport's ping as this one's -- a PINGED or an UNAVAILABLE about a note that was never sent on this lane at all. The scope is the first twelve hex characters of SHA-256 over the resolved bus path, --remote, --branch, --as and --line joined by NUL, and a call whose own scope differs from a standing record's is refused rather than acted on.
func ReadIDs ¶
ReadIDs reads a file of one forge node id per line. This tool posts nothing and composes nothing, so it can only be TOLD which items are this actor's: an id is a fact the poster has and a login is not.
func ReadRest ¶
ReadRest reads --rest's roll. A named line found there is RESTING before anything else is decided -- no draft is sent and no ping is recorded -- and no clock ends it: a rest ends on the line's own return and the window's hand.
func Render ¶
Render turns a stored (key, value) pair back into the line the window reads. It is a pure function of the state, which is what makes rule 11 possible: a record the cap elided is printed by the NEXT call, out of the queue, with no second poll of anything.
now is here for the one field that is a duration rather than a value --- a line's silence --- and for nothing else. Every other field comes out of the value that was observed.
func ReportIdentity ¶
NewOrModified stamps an observed report value with the change kind, which is new when there is no stored value and modified when there is a different one. The kind is in the VALUE so that a record the cap elided renders the same sentence when the next call prints it out of the queue. ReportIdentity is what a report file is COMPARED by: mtime:size, and nothing else. The known limit is named in the spec rather than discovered -- mtime:size cannot see a rewrite that preserves both -- and a limit the spec pins as behaviour is not one the code may quietly close by folding the line count into the compared value: a digest costs a read of every watched file on every poll and is a v2 item behind a flag.
func SplitEntry ¶
SplitEntry reads <repo>#<n> and says what the flag WANTS when it cannot.
func SplitPR ¶
SplitPR reads --pr's <owner/repo>#<n>. It is SplitEntry's shape for SplitEntry's reason: the repository is part of the name.
func SplitRef ¶
SplitRef reads --ref's <owner/repo>:<name>, and says what the flag WANTS when it cannot. The colon is the separator because # names an entry and @ names a head, and a reader of a transcript should be able to tell the three apart at a glance.
func Stamp ¶
Stamp is the one time format this tool writes: RFC 3339 in UTC, from the tool's own clock, never from a time that arrived inside text (rule 9).
func StampOf ¶
StampOf is an RFC 3339 stamp from a git-formatted one, put into UTC the way every other stamp this tool prints is. git's %cI carries the committer's own offset, and a freshness field a reader compares to at= must not read four hours stale at a glance (rule 9: one clock, one spelling).
func TempName ¶
TempName is the one scratch file this tool writes, beside the state it is about to replace. It is named here, in one place, so that the tripwire which proves this package reaches no system scratch directory has one function to read rather than every write site.
func Unreadable ¶
Unreadable reports whether a value is the unreadable one, and its reason. An unreadable value is an ERROR and never a count: it wakes under --final-only exactly as without it, because a flag that asks for fewer wakes about arithmetic is not a flag that asks to sleep through a source that cannot be read.
func ValidateDaemonArgv ¶
ValidateDaemonArgv enforces Johnny's argv-only path boundary: - Direct argv execution only (no shells allowed as binary or arguments) - Recognize one- and two-dash flag spellings, as Go flag parsing does - Reject -c flag - Reject --bodies (must not dump bodies) - Reject --decide (provider invocation, still supported by nova-review) - Reject secondary --as (must not start a second --as or switch identity) - Reject swarm verbs (harvest, fill, native, merge)
func ValidateNotePath ¶
ValidateNotePath checks that the note path exists and is a regular file on the filesystem. If the file is missing or invalid, it returns an error prefixed with "WAKE BROKEN reason=note-file-not-found".
func ValidateNotePaths ¶
ValidateNotePaths validates a slice of note paths.
func WithCommit ¶
WithCommit fills a bus note's empty commit field with the checkout head the poll read, so that what woke the window is named by a sha as well as an id (rule 6).
func WriteSlotLeaseAtomic ¶
WriteSlotLeaseAtomic atomically writes the lease file using a temporary file in the same directory and renaming it into place.
Types ¶
type Advancer ¶ added in v0.12.0
Advancer is the transaction. It holds no state of its own beyond the bus source it advances through and the clock that stamps the marker.
func (*Advancer) Advance ¶ added in v0.12.0
func (a *Advancer) Advance(ctx context.Context, st *State, head string, save func()) (res Result, killed bool, err error)
Advance is step 3, and it is called ONLY behind an empty bus queue and ONLY with no recovery outstanding. The marker is written before the advance and cleared after its output is durable.
func (*Advancer) Recover ¶ added in v0.12.0
Recover is step 4. It runs at the START of a call that finds the marker, and before anything else is polled: a plain `inbox` for carrying=, then the whole carried list, spooled. When the --open read lists FEWER than carrying= -- the checkout moved between the two reads -- it is run once more with the new count; still short, the marker stays, nothing advances, and the call says so.
type Branches ¶
type Branches struct {
Names []string // <owner/repo>:<name>
Every_ time.Duration
Timeout time.Duration
Prev func(key string) (string, bool)
// contains filtered or unexported fields
}
Branches is the --ref source.
type Bus ¶
type Bus struct {
Dir string
As string
ReceiptMaxWords int
Every_ time.Duration
Timeout time.Duration
// Refresh runs `nova-bus wait` instead of `inbox`, with no --advance:
// wait takes the checkout lock, fetches, fast-forwards and returns the
// moment the inbox would list something new or at its timeout. So new mail
// reaches a non-advancing watcher within one poll, no cursor moves and
// nothing is consumed.
Refresh bool
Remote string
Branch string
// ToOnly brings watch into line with serve, which has always dispatched
// only on `to` (rule 10: to means must act, cc means should know, and a
// broadcast to five is five turns). With it, an INBOX NOTE ... addr=cc is
// NOT A CHANGE: not printed, not queued and not marked printed= -- a mark
// would claim the window was shown it -- but COUNTED, cc=<n> on the WAKE
// SOURCE bus line, so the window knows how much is waiting for its next
// natural inbox.
//
// A cc note counts as `suppressed` so rule 7's sum still holds: cc= is a
// BREAKDOWN of suppressed=, never a fourth term, and read = suppressed +
// relayed + standing with the flag as without it.
ToOnly bool
// Seen answers whether this tool has seen an unrecognised line before. It
// is the state's sighting memory, handed in rather than reached for, so
// that the classifier is a pure function of its inputs.
Seen func(line string) bool
// Printed answers whether a note id has already been printed.
Printed func(id string) bool
// contains filtered or unexported fields
}
Bus is the bus-inbox source.
func (*Bus) Budget ¶
Budget is the time this poll may block for: the time to the earliest due source, AT MOST --interval (deprecated/docs/SPEC-WAKE.md, "The bus inbox", --refresh). It is set by the loop before each bus poll, because the earliest due source is a fact about the whole watch and not about this source. --gh-timeout is the budget for a forge call and was never this one: at the documented defaults it would block about nine intervals per poll.
func (*Bus) CC ¶
Counts are rule 7's four numbers, printed once per run before the verdict. They add up: read equals the sum of the other three. A bus that printed nothing and a bus that printed twelve bookkeeping lines must not look the same. CC is the --to-only breakdown of suppressed=.
func (*Bus) Classify ¶
Classify is rule 7 over a transcript the caller has already read: every line suppressed, relayed or standing, every line counted, and the DEFAULT CASE PRINTS. It is exported so that `serve` runs this classifier and not a second one -- the 2026-09-10 hurt was a reader that kept only the tokens it knew about, and one classifier is one place for that to be right.
type Clock ¶
type Clock interface {
Now() time.Time
// Sleep advances to now+d. A real clock waits; a fake one moves its own
// hands, so a sixteen-minute watch is a millisecond of test.
Sleep(d time.Duration)
}
The clock is injected, everywhere, for two reasons. The first is rule 9: every stamp this tool prints or stores is the TOOL's own clock, read at the moment of writing, and a time that reaches it inside text -- a note's body, a report's contents, a caller's flag -- is data and never used to order, age or deduplicate anything. There is no flag that sets a stamp and none will be added. The second is that a watcher's whole subject is elapsed time, and a test that has to wait sixteen real minutes to prove an eight-minute entry interval is a test nobody runs (the two-minute rule).
type Daemon ¶
type Daemon struct {
// contains filtered or unexported fields
}
Daemon enforces Johnny's argv-only path boundary when executing wake commands.
func NewDaemon ¶
func NewDaemon(cfg DaemonConfig) (*Daemon, error)
NewDaemon creates and validates a new Daemon instance.
func (*Daemon) BuildCommand ¶
BuildCommand constructs an exec.Cmd for the daemon using direct argv execution. It verifies the argv boundary and validates that all notePaths exist as regular files.
type DaemonConfig ¶
type DaemonConfig struct {
As string
Command []string
SlotManager *SlotLeaseManager
Stdout io.Writer
Stderr io.Writer
}
DaemonConfig holds settings for a wake daemon instance.
type Entries ¶
type Entries struct {
Names []string // <repo>#<n>, in the order the caller gave them
Every_ time.Duration
Timeout time.Duration
Final bool // --final-only
}
Entries is the entry source.
func (*Entries) Poll ¶
Poll runs gh once per entry, at most EntryBatch outstanding, and turns each answer into a state value. An entry that cannot be read has the state value unreadable:<reason>, which is a state value like any other: the first sighting is a change and the same reason on the next poll does not re-wake.
type EvGithub ¶
type EvGithub struct {
// contains filtered or unexported fields
}
EvGithub reads ev:github, the stream the webhook receiver writes one entry per carried GitHub delivery onto (internal/ghevent), for `nova-wake watch --store` (nova-tools #3876). It only reads: TIP once for a cold cursor, then XREAD BLOCK from the cursor, so a watch on a pull request runs no gh and no git fetch and returns the moment Redis serves a matching entry.
func OpenEvGithub ¶
OpenEvGithub makes the client for addr (host:port, already normalized). user and password are set only when password is non-empty: a store with no ACL is dialled with no credentials, because an empty AUTH is a failed login. No round trip happens here; the first read is the first call.
type Event ¶
type Event struct {
ID, Repo, Number, Kind, Action, Head, Sender, At string
}
Event is one ev:github entry, the fields a watch prints, each trimmed.
type Fake ¶
type Fake struct {
Slept time.Duration
Sleeps int
OnSleep func(now time.Time) // a hook for a test that wants to move the world mid-watch
// contains filtered or unexported fields
}
Fake is the injected clock. It is here rather than in a _test.go file because two packages' tests need the same one -- deprecated/cmd/nova-wake's and this package's -- and two nearly-identical fake clocks is two chances to prove different things.
type Item ¶
type Item struct {
Kind string // bus, busline, entry, report, line, pr, run, branch, lock
Key string
Value string
ID string // the delivery id; empty means derive it from the key and value
// Display is the line the window reads where it carries fields the compared
// identity does not: a pull request's self=, rescan=, push=, by= and url=
// are facts about a PRINTED LINE and not about the value, and folding them
// into the value would make a tick that moved none of them a change.
// Empty means the value is the display.
Display string
// Record keeps this observation out of the delivery queue at observation
// time: the cold-join rule for a pull request that opened mid-run, and
// --not-mine's proved-own tick. It is stored either way -- the suppression
// is of the wake and never of the state.
Record bool
}
Item is one observation.
func (Item) DeliveryID ¶
ID answers an item's delivery id, deriving it where the source did not name one.
type LaneRead ¶
type LaneRead struct {
Dir string // the bus checkout
Ref string // the ref read to: <remote>/<branch> where this call fetched, HEAD where it did not
Lane string // from-<slug>
Anchor string // the ping's anchor: the correlation range's FLOOR and nothing else
Caller string // the name an answer must be addressed to
PingID string // the ping's own id, which a RECEIPTS line may name
MaxItems int
MaxBytes int
Config *bus.Config
// Wall is the deadline on one git process, and Whole the deadline on the
// whole read. Both are bounds the spec names among its five.
Wall time.Duration
Whole time.Duration
// contains filtered or unexported fields
}
LaneRead is one poll's bounded read.
type LaneResult ¶
type LaneResult struct {
Answered bool
AnswerID string // the ANSWERING note's own id, not the ping's
AnswerVia string // "Re line" or "receipt", where the correlation is exact
Complete bool
Remaining string // items between the bookmark and the tip, or "-" past the counting allowance
Gaps int
NewGaps []string // the gaps first recorded this poll: named ONCE, here
Reasons map[string]string
Bookmark string // the seventh field: <commit>:<item>[;<gap>,...]
Progress bool
Notes []string
// What this poll spent, for the assertions the spec's test 19 demands.
Items int
Bytes int
Peak int
Elapsed time.Duration
// contains filtered or unexported fields
}
LaneResult is what one poll of the bounded read learned.
type Lines ¶
type Lines struct {
Bus string
Names []string
After time.Duration
Timeout time.Duration
// Start is when this watch began, and it is what a line with NO commit on
// the branch is judged against: it is OFFLINE at the first poll after the
// watch has run for --offline-after.
Start time.Time
}
Lines is the --line view.
type Locks ¶
type Locks struct {
Paths []string
Every_ time.Duration
// Prev reads the stored value, so the line can carry both ends without
// making an unchanged poll a change (forge.go, withWas).
Prev func(key string) (string, bool)
// contains filtered or unexported fields
}
Locks is the lock source.
type PRs ¶
type PRs struct {
Names []string // <owner/repo>#<n>, named with --pr
Owned []string // <owner/repo>, named with --owned-prs
Every_ time.Duration
Timeout time.Duration
// NotMine holds the forge node ids this actor emitted, re-read at the start
// of each forge tick so an id appended mid-run is set aside from the next
// tick on. Nil means the flag was not given, and then NOTHING is set aside.
NotMine func() (map[string]bool, error)
Prev func(key string) (string, bool)
// contains filtered or unexported fields
}
PRs is the --pr and --owned-prs source.
type Probe ¶
type Probe struct {
Bus string
Line string
As string
Remote, Branch string
Refresh bool
SilentAfter time.Duration
AnswerWithin time.Duration
Interval time.Duration
Timeout time.Duration
PingDraft string
Rest map[string]Rest
CorrelateMax int
CorrelateBytes int
StatePath string
Clock Clock
}
Probe is one `probe --line` call.
type Record ¶
type Record struct {
N int // queue:<n>, taken from queue:next and never reused
ID string
Key string
Value string
}
Record is one undelivered observation: an entry in the delivery queue. A change is delivered when its line has been written to stdout and nothing else, so a record leaves the queue only by being printed (rule 11).
type Reports ¶
Reports is the report-file source. There are NO default report directories: the prototype hardcoded $HOME/deepseek-working-*/jobs/*/RESULT.md and $HOME/freddy-working-*/jobs/*/, which is three guessed paths, a guessed $HOME and two other lines' layouts frozen into a tool.
func (*Reports) Poll ¶
Poll walks each directory. A file that DISAPPEARS is not a change and its key is kept: a job directory being rebuilt is not news, and the window does not want to be woken by an rm.
The state value is mtime:size, and the known limit is named rather than discovered: mtime:size cannot see a rewrite that preserves both, and some tools write identical-length updates within one second. A content digest would close it and costs a read of every watched file on every poll; it is a v2 item behind a flag, and until it exists this paragraph is the answer to "why did it not wake".
type Resp3ConnPool ¶
type Resp3ConnPool struct {
// contains filtered or unexported fields
}
Resp3ConnPool is one RESP3 connection shared by a friend window's heartbeat and its wake. The window never polls: Wait parks on XREAD BLOCK against the wake stream, so an XADD reaches the window as soon as Redis serves it, and Run renews the presence key on the same connection between blocks.
func NewResp3ConnPool ¶
func NewResp3ConnPool(ctx context.Context, addr, presence, stream string, ttl time.Duration) (*Resp3ConnPool, error)
NewResp3ConnPool dials addr with protocol 3 and pins a single connection. presence is the heartbeat key (renewed with ttl); stream is the wake stream.
func (*Resp3ConnPool) Close ¶
func (p *Resp3ConnPool) Close() error
Close releases the shared connection.
func (*Resp3ConnPool) Heartbeat ¶
func (p *Resp3ConnPool) Heartbeat(ctx context.Context) error
Heartbeat renews the presence key on the shared connection.
func (*Resp3ConnPool) Run ¶
func (p *Resp3ConnPool) Run(ctx context.Context, every time.Duration, onWake func(redis.XMessage)) error
Run beats, then blocks for wakes, beat interval at a time, until ctx ends. Heartbeat and wake share the pool's one connection; nothing polls.
type Rest ¶
Rest is one declared rest: the name that rests, the id of the note IN WHICH THE LINE SAID SO, and when -- so the declaration is on the record and a person can open the note this tool does not read.
type Result ¶
type Result struct {
// StandingKeys are the state keys of the lines this poll showed as
// standing, so their recency can be kept: shown every time, woken on once.
StandingKeys []string
// Items are the observations, each with a state key, a state value and a
// delivery id. Whether one is a CHANGE is decided in State.Observe.
Items []Item
// Standing are already-rendered lines that print on every poll they stand
// and are not changes: a bus line this tool has seen before. Shown every
// time, woken on once.
Standing []string
// Notes are WAKE NOTE lines this poll earned, printed once per run.
Notes []string
}
Result is one poll's observation.
type Runs ¶
type Runs struct {
Names []string // <owner/repo>@<sha>
Every_ time.Duration
Timeout time.Duration
Final bool
// Prev reads the stored value, so changed= on the WAKE SOURCE line counts
// what MOVED rather than standing at zero for ever (the Fable read, F5): a
// count nothing increments is a field that cannot go wrong, which is the
// same thing as a field nobody can read.
Prev func(key string) (string, bool)
// contains filtered or unexported fields
}
Runs is the --run source.
type SlotLease ¶
type SlotLease struct {
Owner string `json:"owner"`
PID int `json:"pid"`
Label string `json:"label"`
Until time.Time `json:"until"`
Started time.Time `json:"started"`
Heartbeat time.Time `json:"heartbeat"`
Nonce string `json:"nonce"`
Host string `json:"host"`
Path string `json:"path"`
}
SlotLease represents an on-disk lease record for a wake daemon or runner.
func HeldSlotLease ¶
HeldSlotLease extracts the holder if err is a *SlotLeaseHeldError.
func ParseSlotLease ¶
ParseSlotLease parses key=value lines from a raw byte slice into a SlotLease.
func ReadSlotLease ¶
ReadSlotLease reads and parses the lease file from path or path/.lease if path is a directory.
func (SlotLease) State ¶
State returns "live", "DRIFT", or "expired" based on the lease's Until timestamp and whether the holding PID is still alive.
Liveness semantics: - If Until is in the future AND PID is alive: "live" - If Until has expired AND PID is STILL ALIVE: "DRIFT" (DRIFT preservation: must not be reclaimed!) - If PID is dead (regardless of Until, or past Until): "expired" (dead PID reclaimable)
type SlotLeaseConfig ¶
type SlotLeaseConfig struct {
Dir string
LeasePath string
Owner string
Label string
TTL time.Duration
HeartbeatInterval time.Duration
PID int
PIDAlive func(pid int) bool
Clock Clock
}
SlotLeaseConfig contains configuration for acquiring and maintaining a slot lease.
type SlotLeaseHeldError ¶
SlotLeaseHeldError is returned when attempting to acquire a lease that is currently held by a live process or preserved under DRIFT.
func (*SlotLeaseHeldError) Error ¶
func (e *SlotLeaseHeldError) Error() string
type SlotLeaseManager ¶
type SlotLeaseManager struct {
// contains filtered or unexported fields
}
SlotLeaseManager coordinates slot lease acquisition, background heartbeats, and fenced release.
func AcquireSlotLease ¶
func AcquireSlotLease(cfg SlotLeaseConfig) (*SlotLeaseManager, error)
AcquireSlotLease attempts to acquire a slot lease under cfg.
Rules enforced:
- If an existing lease is "live" (held by another alive process): refused.
- If an existing lease is in "DRIFT" (expired Until, but PID is STILL ALIVE): DRIFT IS PRESERVED: refused, never stolen or reclaimed!
- If an existing lease is "expired" (PID is dead): DEAD PID IS RECLAIMED: the dead holder's lease is overwritten atomically.
- Default TTL is 15s; default heartbeat is 5s.
- Writes are atomic via temp file + rename.
func (*SlotLeaseManager) Close ¶
func (m *SlotLeaseManager) Close() error
Close is an alias for Release.
func (*SlotLeaseManager) Heartbeat ¶
func (m *SlotLeaseManager) Heartbeat() error
Heartbeat performs an immediate synchronous lease renewal.
func (*SlotLeaseManager) Lease ¶
func (m *SlotLeaseManager) Lease() SlotLease
Lease returns a copy of the currently held lease record.
func (*SlotLeaseManager) Release ¶
func (m *SlotLeaseManager) Release() error
Release stops the heartbeat loop, waits for it to exit, and removes the lease file ONLY if the file on disk still matches this manager's PID and nonce.
type Source ¶
type Source interface {
// Name is the source's name in fail:<source> and on WAKE BROKEN and WAKE
// POLL: bus, entries or reports.
Name() string
// Every is this source's poll cadence. The bus's is --interval and an
// entry's is --entry-interval, because an 8-minute hosted run deserves one
// check at 8 minutes and not eight checks at one minute.
Every() time.Duration
// Poll observes the world once. It returns the items whose value may have
// moved -- the comparison is the caller's -- and the lines that are shown
// on every poll they stand and woken on once.
Poll(ctx context.Context, now time.Time) (Result, error)
}
A source is a thing with a STATE VALUE and a rule for what makes two state values different. That is the whole design, and it is why there is one comparison in one place (State.Observe) rather than one per source: a fourth source cannot invent its own idea of what a change is.
type State ¶
type State struct {
// contains filtered or unexported fields
}
State is nova-wake's own file, in a format nova-wake chose. It is not a record of anything: delete it and you get a cold start, which is a correct if noisier watch. Nothing else may read it as truth.
func Load ¶
Load reads a state file. A file that is not there is a cold start; a file that is there and cannot be parsed is an ERROR and never a silent cold start, because a run that starts cold on an unreadable state has swallowed everything that moved while no watcher was running and has a correct-looking first poll.
func (*State) Cold ¶
Cold reports whether there was no state file at all. A first run is not a change (see the cold-start rule), and cold=true on the opening line says the run is in that state.
func (*State) Evict ¶
func (s *State) Evict()
Evict holds the two event-shaped parts of the state to LRUMax entries each, least recently SEEN first, with every key a queue record names exempt.
func (*State) Fail ¶
Fail records one failed poll of a source and returns the streak, the stamp the streak started at and the reason. The streak LIVES IN THE STATE FILE and spans calls: a counter that started at zero on every call would never reach three over a source whose error text changes, and three in a row is what ends a watch loudly (rule 8).
func (*State) Get ¶
Get returns a plain (unwatched) entry: bus:advance, fail:<source>, serve:<id>, queue:next and the queue records themselves.
func (*State) Keys ¶
Keys returns every key in the state, sorted, so that two saves of one map are byte-identical and a diff of two state files is readable.
func (*State) MarkPrinted ¶
MarkPrinted deletes a record and writes printed=<its id> for its key. It is called ONLY after the record's line reached stdout (rule 11, step 3): a kill before this leaves the entry pending, so the duplicate is a repeated wake and never a lost one.
func (*State) Observe ¶
Observe compares a freshly polled value against the stored newest one, byte for byte, and reports whether this is a change.
A change APPENDS a queue record and then replaces the newest value. It never replaces an UNPRINTED value, because the unprinted value is in the queue and a queue record leaves only by being printed: red then green with the red unprinted prints red, then green, in that order. Nothing coalesces.
func (*State) ObserveDisplay ¶
ObserveDisplay is Observe where the line the window reads carries one field more than the identity does. A report file is the case: its identity is mtime:size and nothing else, because a poll that found the same file must be quiet -- and its LINE says new or modified, which is a fact about the stored value rather than about the file. Folding that word into the compared value would make every second poll of an unchanged file a change, which is the false wake of 2026-09-11 wearing different clothes.
func (*State) Pending ¶
Pending is the length of the delivery queue: every observation this run or an earlier one made and has not printed.
func (*State) PrintedID ¶
PrintedID is the delivery id of the value the window was last SHOWN for this key, or "-" when it has been shown none.
func (*State) Queue ¶
Queue returns the undelivered records in <n> order: oldest first, which is the order they are printed in and the reason a line elided by one call's cap is printed by the next before anything newer.
func (*State) RecordOnly ¶
Record writes a watched value with no queue record and no change: the cold-start rule's "records the world and reports nothing", and --final-only's suppression at observation time.
func (*State) Save ¶
Save writes the whole map through a temporary file in the same directory and an atomic rename, so a call killed by the harness mid-poll leaves either the previous state or the new one and never half of either.
The temp file has a FIXED name beside the state rather than a random one in a system scratch directory: rule 4 says the only files this tool touches are --state, the temp file beside it and <state>.lock, and a rename is only atomic within one filesystem anyway.
func (*State) Sight ¶
Sight records an unrecognised bus line as SEEN: the watched form every other key is stored in, and the recency the LRU evicts by. It is the writer for a line that is only SEEN -- serve's sightings and a standing line the watch re-sights; a line the watch relays as news goes through ObserveDisplay, which touches the same recency. EVERY writer of a bus:line: row must touch, because a write that left the recency alone is what made the bound a sort by key name -- and a bound by key name deletes the same earliest-sorting lines every poll and wakes the window with them again, which is the prototype's item 9 in a new costume.