wake

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 26 Imported by: 0

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

View Source
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"
)
View Source
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.

View Source
const (
	StatePresent      = "PRESENT"
	StateSilent       = "SILENT"
	StatePinged       = "PINGED"
	StateAnswered     = "ANSWERED"
	StateUnavailable  = "UNAVAILABLE"
	StateResting      = "RESTING"
	StateUnreconciled = "UNRECONCILED"
)

The seven state words.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
const EvGithubStream = gheventwire.Stream

EvGithubStream is the stream name, the one ghevent writes.

View Source
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.

View Source
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.

View Source
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.

View Source
const OwnedCap = 20

OwnedCap is the listing law over one repository's owned pull requests.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
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.

View Source
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

func AcceptBus(tool, found string) bool

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

func BusCarriedEntries(out string) int

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

func BusNoteIDs(out string) []string

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

func BusQueued(st *State) int

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

func BusRefusal(tool, found string) string

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

func BusVersion(ctx context.Context, timeout time.Duration) (string, error)

BusVersion runs `nova-bus version` and reads the second token of its first line. It is run ONCE, before the opening line.

func CapKind

func CapKind(kind string) string

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

func Compose(parts ...string) string

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

func Decompose(s string) []string

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

func DeliveryID(key, value string) string

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

func Dur(d time.Duration) string

Dur is the one duration spelling this tool prints: Go's own, which is unambiguous and parses back with time.ParseDuration.

func DurShort

func DurShort(d time.Duration) string

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

func FormatSlotLease(l *SlotLease) string

FormatSlotLease formats the lease into key=value lines.

func Head(ctx context.Context, dir string, timeout time.Duration) (sha, at string)

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

func HereLine(now time.Time, b Bench, quietLoad string) string

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

func Interrupted(st *State) bool

Interrupted reports whether an advance was in flight when the last call died.

func IsFinal

func IsFinal(value string) bool

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

func IsFinalRun(value string) bool

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

func IsForbiddenShell(name string) bool

IsForbiddenShell reports whether a binary name represents a shell.

func IsForbiddenSwarmVerb

func IsForbiddenSwarmVerb(arg string) bool

IsForbiddenSwarmVerb reports whether an argument represents a forbidden swarm verb.

func LineIsOffline

func LineIsOffline(value string) bool

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

func LockAdvance(busDir, as string) (release func(), holder string, err error)

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

func LockName(path string) string

LockName is the lock file beside the state, holding the pid of the run that took it (rule 4).

func LockState

func LockState(path string) (release func(), holder string, err error)

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 NewOrModified(value string, had bool) string

func ProbeScope

func ProbeScope(busDir, remote, branch, as, line string) string

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

func ReadIDs(path string) (map[string]bool, error)

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

func ReadRest(path string) (map[string]Rest, error)

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

func Render(kind, key, value string, now time.Time) string

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

func ReportIdentity(value string) string

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

func SplitEntry(name string) (repo, number string, err error)

SplitEntry reads <repo>#<n> and says what the flag WANTS when it cannot.

func SplitHead

func SplitHead(name string) (repo, sha string, err error)

SplitHead reads --run's <owner/repo>@<sha>.

func SplitPR

func SplitPR(name string) (repo, number string, err error)

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

func SplitRef(name string) (repo, branch string, err error)

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

func Stamp(t time.Time) string

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

func StampOf(s string) string

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

func TempName(path string) string

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

func Unreadable(value string) (bool, string)

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

func ValidateDaemonArgv(argv []string, targetAs string) error

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

func ValidateNotePath(notePath string) error

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

func ValidateNotePaths(notePaths []string) error

ValidateNotePaths validates a slice of note paths.

func WithCommit

func WithCommit(value, head string) string

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

func WriteSlotLeaseAtomic(leasePath string, lease *SlotLease) error

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

type Advancer struct {
	Bus   *Bus
	Clock Clock
}

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

func (a *Advancer) Recover(ctx context.Context, st *State) (Result, string, error)

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 Bench

type Bench struct {
	Load     [3]float64
	HasLoad  bool
	CPUs     int
	Procs    int
	HasProcs bool
}

Bench is one reading of this machine.

func ReadBench

func ReadBench() Bench

ReadBench takes the three numbers at this instant.

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.

func (*Branches) Calls

func (c *Branches) Calls() int

func (*Branches) Counts

func (b *Branches) Counts() (read, changed, unreadable, calls int)

func (*Branches) Every

func (b *Branches) Every() time.Duration

func (*Branches) Name

func (b *Branches) Name() string

func (*Branches) Poll

func (b *Branches) Poll(ctx context.Context, now time.Time) (Result, error)

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

func (b *Bus) Budget(d time.Duration)

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

func (b *Bus) CC() int

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

func (b *Bus) Classify(out string) Result

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.

func (*Bus) Counts

func (b *Bus) Counts() (read, suppress, relay, standing int)

func (*Bus) Every

func (b *Bus) Every() time.Duration

func (*Bus) Name

func (b *Bus) Name() string

func (*Bus) Poll

func (b *Bus) Poll(ctx context.Context, now time.Time) (Result, error)

Poll runs one bus read and classifies every line of it.

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

func (d *Daemon) BuildCommand(ctx context.Context, notePaths ...string) (*exec.Cmd, error)

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.

func (*Daemon) Close

func (d *Daemon) Close() error

Close releases any held slot lease.

func (*Daemon) Dispatch

func (d *Daemon) Dispatch(ctx context.Context, notePaths ...string) error

Dispatch executes the daemon's command with the validated notePaths.

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) Every

func (e *Entries) Every() time.Duration

func (*Entries) Name

func (e *Entries) Name() string

func (*Entries) Poll

func (e *Entries) Poll(ctx context.Context, now time.Time) (Result, error)

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

func OpenEvGithub(addr, user, password string) *EvGithub

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.

func (*EvGithub) Close

func (e *EvGithub) Close() error

Close releases the client.

func (*EvGithub) Read

func (e *EvGithub) Read(ctx context.Context, cursor string, count int64, block time.Duration) ([]Event, error)

Read blocks up to block for entries after cursor, at most count of them. A block that ends with nothing is (nil, nil), not an error.

func (*EvGithub) Tip

func (e *EvGithub) Tip(ctx context.Context) (string, error)

Tip is the id of the newest entry, or "0-0" when the stream is empty or absent: the cursor a cold watch starts from, so history is not news.

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.

func NewFake

func NewFake(at time.Time) *Fake

NewFake starts a fake clock at the instant given.

func (*Fake) Advance

func (f *Fake) Advance(d time.Duration)

Advance moves a fake clock without counting a sleep, for a test that wants to put a gap BETWEEN two calls.

func (*Fake) Now

func (f *Fake) Now() time.Time

func (*Fake) Sleep

func (f *Fake) Sleep(d time.Duration)

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

func (i Item) DeliveryID() string

ID answers an item's delivery id, deriving it where the source did not name one.

func (Item) Shown

func (i Item) Shown() string

Shown is the line's own form of an item, which is Display where the source gave one and the value otherwise.

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.

func (*LaneRead) Run

func (r *LaneRead) Run(ctx context.Context, bookmark string) LaneResult

Run performs one poll's read, resuming from the bookmark the record carries.

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.

func (*Lines) Poll

func (l *Lines) Poll(ctx context.Context, now time.Time) (Result, error)

Poll reads the checkout's log once, read-only, under the timeout, and nothing else. The sign is a COMMIT STAMP and never a date in a commit's subject: a time that reaches this tool inside text is data (rule 9).

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.

func (*Locks) Counts

func (l *Locks) Counts() (read, changed, unreadable int)

Counts are the WAKE SOURCE locks line's. There is no calls= and no login=: the lock source starts nothing at all, and a login is the prs source's fact.

func (*Locks) Every

func (l *Locks) Every() time.Duration

func (*Locks) Name

func (l *Locks) Name() string

func (*Locks) Poll

func (l *Locks) Poll(ctx context.Context, now time.Time) (Result, error)

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.

func (*PRs) Calls

func (c *PRs) Calls() int

func (*PRs) Counts

func (p *PRs) Counts() (read, changed, unreadable, calls, self int, login string)

func (*PRs) Every

func (p *PRs) Every() time.Duration

func (*PRs) Name

func (p *PRs) Name() string

func (*PRs) Poll

func (p *PRs) Poll(ctx context.Context, now time.Time) (Result, error)

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.

func (*Probe) Run

func (p *Probe) Run(ctx context.Context, st *State, stdout, stderr io.Writer) int

Run is the whole verb. It answers the process exit code: 0 where work can be handed over now, 1 where it cannot, 2 where the call could not run.

type Real

type Real struct{}

Real is the clock a watch runs on.

func (Real) Now

func (Real) Now() time.Time

func (Real) Sleep

func (Real) Sleep(d time.Duration)

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

type Reports struct {
	Dirs   []string
	Every_ time.Duration
}

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) Every

func (r *Reports) Every() time.Duration

func (*Reports) Name

func (r *Reports) Name() string

func (*Reports) Poll

func (r *Reports) Poll(ctx context.Context, now time.Time) (Result, error)

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.

func (*Resp3ConnPool) Subscribe

func (p *Resp3ConnPool) Subscribe(ctx context.Context) error

Subscribe anchors the wake cursor at the stream's current tail, so every XADD after Subscribe returns is delivered by Wait and none before it is.

func (*Resp3ConnPool) Wait

func (p *Resp3ConnPool) Wait(ctx context.Context, block time.Duration) ([]redis.XMessage, error)

Wait parks on XREAD BLOCK for up to block and returns the wakes that arrived. A timeout with no wake returns (nil, nil).

type Rest

type Rest struct {
	Name  string
	Note  string
	Stamp string
}

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.

func (*Runs) Calls

func (c *Runs) Calls() int

func (*Runs) Counts

func (r *Runs) Counts() (read, changed, unreadable, calls int)

func (*Runs) Every

func (r *Runs) Every() time.Duration

func (*Runs) Name

func (r *Runs) Name() string

func (*Runs) Poll

func (r *Runs) Poll(ctx context.Context, now time.Time) (Result, error)

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

func HeldSlotLease(err error) (SlotLease, bool)

HeldSlotLease extracts the holder if err is a *SlotLeaseHeldError.

func ParseSlotLease

func ParseSlotLease(raw []byte, path string) (*SlotLease, error)

ParseSlotLease parses key=value lines from a raw byte slice into a SlotLease.

func ReadSlotLease

func ReadSlotLease(target string) (*SlotLease, error)

ReadSlotLease reads and parses the lease file from path or path/.lease if path is a directory.

func (SlotLease) State

func (l SlotLease) State(now time.Time) string

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)

func (SlotLease) StateWithAlive

func (l SlotLease) StateWithAlive(now time.Time, isAlive func(int) bool) string

StateWithAlive evaluates lease state with a custom PID liveness function.

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

type SlotLeaseHeldError struct {
	Holder SlotLease
	State  string // "live" or "DRIFT"
	Reason string
}

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:

  1. If an existing lease is "live" (held by another alive process): refused.
  2. If an existing lease is in "DRIFT" (expired Until, but PID is STILL ALIVE): DRIFT IS PRESERVED: refused, never stolen or reclaimed!
  3. If an existing lease is "expired" (PID is dead): DEAD PID IS RECLAIMED: the dead holder's lease is overwritten atomically.
  4. Default TTL is 15s; default heartbeat is 5s.
  5. 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

func Load(path string) (*State, error)

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) ClearFail

func (s *State) ClearFail(source string)

ClearFail is what a successful poll does to the streak.

func (*State) Cold

func (s *State) Cold() bool

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) Delete

func (s *State) Delete(key string)

Delete removes an entry.

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

func (s *State) Fail(source, reason string, now time.Time) (n int, since, last string)

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

func (s *State) Get(key string) (string, bool)

Get returns a plain (unwatched) entry: bus:advance, fail:<source>, serve:<id>, queue:next and the queue records themselves.

func (*State) IsPending

func (s *State) IsPending(key string) bool

IsPending reports whether any queue record names this key.

func (*State) Keys

func (s *State) Keys() []string

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

func (s *State) MarkPrinted(r Record)

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) Newest

func (s *State) Newest(key string) (string, bool)

Newest is the newest observed state value of a watched key.

func (*State) Observe

func (s *State) Observe(key, value, id string) bool

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

func (s *State) ObserveDisplay(key, value, display, id string) bool

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

func (s *State) Pending() int

Pending is the length of the delivery queue: every observation this run or an earlier one made and has not printed.

func (*State) PrintedID

func (s *State) PrintedID(key string) string

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

func (s *State) Queue() []Record

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

func (s *State) RecordOnly(key, value string)

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

func (s *State) Save(path string) error

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) Set

func (s *State) Set(key, value string)

Set writes a plain entry.

func (*State) Sight

func (s *State) Sight(key string)

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.

func (*State) Streak

func (s *State) Streak(source string) (n int, since, last string)

Streak reads a source's standing failure streak without adding to it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL