Documentation
¶
Overview ¶
Package teamrouter is the Go port of the Orca attention router (hadron-server scripts/orca-attention-router, TypeScript) — cli#829 (T7).
It nudges the Orca-hosted workers on an explicit roster when they are mentioned, by typing one short line into that worker's Orca terminal. It never reads, relays or posts a chat message, never marks anything read, and never hands its token to a worker.
The acceptance spec is the TS suite, ported 1:1 into this package's tests (at server main f0ab31a0 — #1845's per-roster terminal pin). The TS router shelled out to `hadron api`; this port speaks GraphQL natively through the Server seam and keeps exec-ing only the `orca` CLI.
Why the gates exist (finding hrn:node:hadronmemory.com:hadron-server:findings:orca-terminal-send-busy-agent-claude-queues-codex-steers):
- A prompt typed into a BUSY Codex agent steers its turn and drops the rest of its task. So a nudge is sent only on proof the terminal is idle; an UNTRACKED terminal can never satisfy tui-idle, so its timeout is unknown, and the rendered screen decides (#7398).
- tui-idle reports idle while a HUMAN is typing, so the screen must also prove the prompt empty (#5473), with a stale Orca draft refuted by a Claude screen (#6736).
- Tab titles are not identity: each roster row names its worktree, and a CLI worker binding in that worktree must agree.
- The token is adopted only when every roster worker with new attention was nudged; a ledger keyed on it stops the retry re-nudging the others.
- Counts are recomputed tokenless immediately before the send (#1616), and both gates are re-proved after that round trip, the idle one fail-closed.
Index ¶
- func BreakStaleLock(lockDir, staleOwner string) (string, error)
- func CheckRosterEntry(c *Config, entry RosterEntry) error
- func DefaultOrcaCli() string
- func DescribeOutcome(o *Outcome) string
- func LivenessNudgeText(cursor, head int) string
- func NudgeText(a AttentionItem) string
- func PromptLineIsEmpty(lines []string, family string) bool
- func PutRosterEntry(path string, entry RosterEntry) (string, error)
- func RecordReload(c *Config) error
- func ResolveTerminal(entry RosterEntry, terminals []Terminal, c *Config, orca OrcaRunner) (handle, family, how string, err error)
- func ScreenAgentFamily(sc *Screen) string
- type AttentionItem
- type AttentionPage
- type AttentionRequest
- type Config
- type CursorResult
- type GraphQLServer
- func (s *GraphQLServer) AttentionPage(ctx context.Context, req AttentionRequest) (AttentionPage, error)
- func (s *GraphQLServer) ChannelHead(ctx context.Context, channelRef string) (int, error)
- func (s *GraphQLServer) ReadCursor(ctx context.Context, workerID string) (CursorResult, error)
- func (s *GraphQLServer) RegisterRow(ctx context.Context, workerID string) (RegisterRow, error)
- type Ledger
- type LedgerNudge
- type LivenessRecord
- type LivenessStatus
- type OrcaRunner
- type Outcome
- type RegisterRow
- type ReloadReceipt
- type RosterEntry
- type RouterError
- type RunResult
- type Screen
- type Server
- type State
- type StatusReport
- type Terminal
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BreakStaleLock ¶
BreakStaleLock is breakStaleLock for a lock judged stale by its owner record staleOwner: "retry" (broken, or already gone — take it) or "busy" (another run took it under the stale judgement — stand down). The rename protocol never fails outright, so err is always nil; it is in the signature for the callers that treat a lock break as fallible.
func CheckRosterEntry ¶
func CheckRosterEntry(c *Config, entry RosterEntry) error
CheckRosterEntry performs the same identity checks used under the writer lock.
func DefaultOrcaCli ¶
func DefaultOrcaCli() string
DefaultOrcaCli is Orca's bundled CLI on macOS; elsewhere `orca` on PATH.
func DescribeOutcome ¶
DescribeOutcome is the one-line summary of a poll; never a token or a message body.
func LivenessNudgeText ¶
LivenessNudgeText is the one line a FLAGGED worker receives from the liveness rule (#1826). Its own literal, never NudgeText's: it makes NO mention-count claim, names its rule (Taylor #9748), names the two positions it is true for — both read fresh immediately before the send — and anchors an unfiltered catch-up from the watermark. "Mark read" appears exactly once.
func NudgeText ¶
func NudgeText(a AttentionItem) string
NudgeText is the one line a worker receives: fresh counts with the seq range they cover — never a message body, never a token. The seqs let the recipient check the nudge against its own read cursor (#1616).
Two anchors (#1811): at or below the large-gap span, the #1805 watermark anchor (start at the first unread, walk forward); above it, a BACKWARD walk of the mentions filter from the head THIS NUDGE NAMED (frozen as every page's --before ceiling, #1827) down to the first unread, then a mark at the highest seq actually delivered — never above the named head.
func PromptLineIsEmpty ¶
PromptLineIsEmpty is true only on positive proof the agent's prompt holds no typed text (#5473: tui-idle reports idle while a HUMAN is typing). Both agents draw their real chrome at column 0 and indent anything typed onto a continuation line, so only column-0 lines count as chrome.
- claude: the box between the last two column-0 rules is a bare `❯` plus blank lines, and the ONE nonblank line below it — the terminal's last content — ends with the empty-input hint (typed whitespace renders as nothing; trailing content means the box is history, #1823).
- codex: the last column-0 `›` line shows the idle placeholder and every line below it is footer.
Anything unrecognized — no box, no marker, an unknown family — is NOT empty.
func PutRosterEntry ¶
func PutRosterEntry(path string, entry RosterEntry) (string, error)
PutRosterEntry preserves unknown config fields and refuses a reassignment. The caller holds its onboarding lock for the complete operation sequence; the router writer lock additionally excludes an in-flight delivery here.
func RecordReload ¶
func ResolveTerminal ¶
func ResolveTerminal(entry RosterEntry, terminals []Terminal, c *Config, orca OrcaRunner) (handle, family, how string, err error)
ResolveTerminal maps a roster row to the ONE agent terminal in its worktree: (handle, family, how), how being "pin" (#1845), "identity", or "screen" (the #1823 identity-flap fallback). An unresolvable row returns how "none" and an error whose text is the hold reason.
A pin resolves first but is trusted only within the row's worktree, connected and writable. Identity is the terminal whose agentIdentity is claude/codex. Only when identity resolves NOTHING does the fallback read screens: ONE connected terminal whose screen positively shows a family. Ambiguity, an unclassifiable screen or a plain shell holds — never a guess.
func ScreenAgentFamily ¶
ScreenAgentFamily is the agent family a rendered screen POSITIVELY shows, or "" — the identity-flap fallback's evidence (#1823). Only the agents' own chrome counts: anchored banner lines, column-0 `✻`/`⏺`/`permissions:`, Claude's pair of rules, and Codex's `›` only WITH content after it (a bare glyph is a shell theme as easily as a composer). Both families on one screen reads "": hold, never guess.
Types ¶
type AttentionItem ¶
type AttentionItem struct {
Worker string `json:"worker"`
WorkerName string `json:"workerName"`
Channel string `json:"channel"`
Unread int `json:"unread"`
UnreadMentions int `json:"unreadMentions"`
FirstUnreadSeq *int `json:"firstUnreadSeq"`
// LastRelevantSeq is the highest seq that counts for this worker: for a
// mention-only row, its latest mention. 0 = none.
LastRelevantSeq int `json:"lastRelevantSeq"`
LastSeq int `json:"lastSeq"`
}
AttentionItem is one Worker/Channel pair from the paged attention scan (TeamAttentionPageItem). LastRelevantSeq 0 stands for the server's null: seqs start at 1, so no real mention frontier is 0.
type AttentionPage ¶
type AttentionPage struct {
ScanComplete bool `json:"scanComplete"`
NextPage string `json:"nextPage"`
AdoptableSince string `json:"adoptableSince"`
Items []AttentionItem `json:"items"`
}
AttentionPage is one page of the attention scan.
type AttentionRequest ¶
AttentionRequest addresses the paged surface: Since passes the adopted token, Page walks pages. Both empty is a tokenless scan — the first poll, and the fresh re-read before each send (#1616).
type Config ¶
type Config struct {
// Fingerprint identifies the exact bytes loaded, without exposing state tokens.
Fingerprint string `json:"-"`
App string `json:"app"`
Channel string `json:"channel"`
OrcaCli string `json:"orcaCli"`
StateDir string `json:"stateDir"`
Roster []RosterEntry `json:"roster"`
// BusyWaitMS is the idle gate's tui-idle wait (default 60 s).
BusyWaitMS int `json:"busyWaitMs"`
// SubmitWaitS is `orca terminal send --wait-submit` (default 15 s).
SubmitWaitS int `json:"submitWaitS"`
// CoordinatorWatchMin is the #1826 liveness rule's N: minutes of
// continuous idle a flagged worker must exceed (strictly) before its
// liveness nudge may fire. Positive and finite; default 20.
CoordinatorWatchMin float64 `json:"coordinatorWatchMinutes"`
}
Config is the router's host-local configuration. Its JSON keys are router.ts's, so an existing ~/.config/hadron-orca-attention-router config loads unchanged (cli#829: config migrates). `hadronCli` is accepted and ignored: this port speaks GraphQL natively.
func LoadConfig ¶
LoadConfig reads and validates the router config. Validation is strict where a permissive read once passed a typo silently: roster entries need worker, name and worktree; the watch flags are JSON booleans; a pin is a non-empty string; no worker, pin or worktree appears twice; numbers are JSON numbers.
type CursorResult ¶
CursorResult is a worker's channelReadState (#1826) — the liveness source attention cannot see. FieldErrs carries GraphQL errors that arrived WITH data: an integer alongside errors is not a verdict (#1829 P1-1).
type GraphQLServer ¶
type GraphQLServer struct {
// contains filtered or unexported fields
}
GraphQLServer is the production Server: the router's own typed reads (internal/api/queries/teamrouter.graphql) on the operator's login. Every method is a read; none takes or returns the adopted token except AttentionPage, which passes it to the server and never logs it.
func NewGraphQLServer ¶
func NewGraphQLServer(client graphql.Client, app, channel string) *GraphQLServer
NewGraphQLServer reads app's attention for the team Channel channel (an id).
func (*GraphQLServer) AttentionPage ¶
func (s *GraphQLServer) AttentionPage(ctx context.Context, req AttentionRequest) (AttentionPage, error)
AttentionPage reads one page of the paged attention scan.
func (*GraphQLServer) ChannelHead ¶
ChannelHead is the Channel's watermark.
func (*GraphQLServer) ReadCursor ¶
func (s *GraphQLServer) ReadCursor(ctx context.Context, workerID string) (CursorResult, error)
ReadCursor is the worker's read cursor on the team Channel. Any GraphQL error — including errors that arrive WITH data — is an error here, so the router reads it as unknown (#1829 P1-1).
func (*GraphQLServer) RegisterRow ¶
func (s *GraphQLServer) RegisterRow(ctx context.Context, workerID string) (RegisterRow, error)
RegisterRow is the worker's winning register row on the team Channel, or the zero row (which never counts as mention-only) when it has none there.
type Ledger ¶
type Ledger struct {
Base string `json:"base"`
Nudged map[string]LedgerNudge `json:"nudged"`
// Reported is each held worker's last LOGGED reason (by name), so a hold
// is logged once per token, not once per poll.
Reported map[string]string `json:"reported"`
// DraftNotes: stale-draft discards already logged under this token.
DraftNotes map[string]string `json:"draftNotes"`
// HeldSince: epoch ms a worker's current hold began (cleared on delivery).
HeldSince map[string]int64 `json:"heldSince"`
// HeldLoggedAt: epoch ms the last long-hold line was logged per worker.
HeldLoggedAt map[string]int64 `json:"heldLoggedAt"`
}
Ledger is the durable delivery record for the adopted token: which workers were already nudged for the attention reported since that token, so a poll that must hold the token does not nudge them again.
type LedgerNudge ¶
type LedgerNudge struct {
// RelevantSeq is the worker's last RELEVANT (mention) seq the nudge was about.
RelevantSeq int `json:"relevantSeq"`
At int64 `json:"at"`
Stages string `json:"stages"`
}
LedgerNudge records a delivered (or dropped-as-read) nudge's mention frontier.
type LivenessRecord ¶
type LivenessRecord struct {
// IdleSince is the epoch ms the continuous idle episode began; nil while reset.
IdleSince *int64 `json:"idleSince,omitempty"`
// Handle is the terminal the clock is measured on; a change is a replaced
// terminal, which resets the clock.
Handle string `json:"handle,omitempty"`
// CoveredFrontier is the channel head at the last DELIVERED liveness
// nudge; re-eligibility needs head > this.
CoveredFrontier *int `json:"coveredFrontier,omitempty"`
}
LivenessRecord is one flagged worker's liveness clock (#1826). Deliberately a SEPARATE file from the ledger: adoption deletes the ledger, but an idle episode must survive token adoption and router restarts untouched.
type LivenessStatus ¶
type LivenessStatus struct {
IdleMinutes *int64 `json:"idleMinutes"`
CoveredFrontier *int `json:"coveredFrontier"`
}
LivenessStatus is one flagged worker's watch state.
type OrcaRunner ¶
OrcaRunner execs the `orca` CLI. argv starts at the verb ("terminal", "list", …): the binary is the runner's business, so a fake never sees it.
func ExecOrca ¶
func ExecOrca(bin string) OrcaRunner
ExecOrca is the production runner: it execs bin with argv and never errors on a non-zero exit — the envelope (or its absence) is the verdict.
type Outcome ¶
type Outcome struct {
Nudged []string `json:"nudged"`
// Dropped: workers that read their chat themselves while the nudge waited (#1616).
Dropped []string `json:"dropped"`
Covered []string `json:"covered"`
Held map[string]string `json:"held"`
Adopted bool `json:"adopted"`
// Liveness: flagged workers the liveness rule woke this poll (#1826).
Liveness []string `json:"liveness"`
// LivenessDropped: flagged workers that caught up while a liveness nudge
// waited out its gates.
LivenessDropped []string `json:"livenessDropped"`
}
Outcome is one poll's result. Held maps a worker's NAME to why it was not nudged; any hold keeps the token unadopted.
func PollOnce ¶
func PollOnce(ctx context.Context, c *Config, s Server, orca OrcaRunner, log func(string), now func() int64) (*Outcome, error)
PollOnce polls attention, nudges each idle roster worker with new mentions, adopts the token only if all were — then runs the #1826 liveness pass over the flagged roster. now is the clock (epoch ms), injectable for the strict >N boundary; nil means the wall clock. A *RouterError leaves the adopted token unchanged.
type RegisterRow ¶
type RegisterRow struct {
NamedBy string `json:"namedBy"`
ChannelID string `json:"channel"`
MentionOnly bool `json:"mentionOnly"`
OwnerType string `json:"ownerType,omitempty"`
}
RegisterRow is the WINNING attendeeRegister row for the team Channel, as the attention API resolves it (App over organization; Worker over Agent over everyone).
type ReloadReceipt ¶
ReloadReceipt confirms a loop loaded these exact config bytes between polls.
func ReadReload ¶
func ReadReload(c *Config) (ReloadReceipt, error)
type RosterEntry ¶
type RosterEntry struct {
// Worker is the Worker's permanent id.
Worker string `json:"worker"`
Name string `json:"name"`
// Worktree is the Orca worktree this worker is driven in.
Worktree string `json:"worktree"`
// Terminal (#1845) pins the handle of this worker's MAIN terminal. A pin
// resolves FIRST, but only ever to a terminal IN this row's worktree,
// connected and writable — a stale pin falls through with its reason.
Terminal string `json:"terminal,omitempty"`
// Coordinator / Lifecycle (#1826) opt the row into the liveness rule:
// a watch a passive wait can strand.
Coordinator bool `json:"coordinator,omitempty"`
Lifecycle bool `json:"lifecycle,omitempty"`
}
RosterEntry names one worker this router may wake.
type RouterError ¶
type RouterError struct {
Reason string
}
RouterError is a failure that leaves the adopted token untouched: the poll changes nothing it has not already persisted, and the next poll retries.
func (*RouterError) Error ¶
func (e *RouterError) Error() string
type Screen ¶
Screen is one `orca terminal read --screen` result: the rendered tail, the optional draft field, and the source (only "screen" is a full frame — the default stream read returns repaint fragments, useless for shape checks).
type Server ¶
type Server interface {
AttentionPage(ctx context.Context, req AttentionRequest) (AttentionPage, error)
RegisterRow(ctx context.Context, workerID string) (RegisterRow, error)
ReadCursor(ctx context.Context, workerID string) (CursorResult, error)
ChannelHead(ctx context.Context, channelRef string) (int, error)
}
Server is the typed GraphQL seam (native, never `hadron api`). Every method is a read: the router changes no server state.
type State ¶
type State struct {
// contains filtered or unexported fields
}
State is the token/ledger/liveness persistence (dir 0700, files 0600).
func (*State) Ledger ¶
Ledger is the ledger for the current token; a stale (other token), unreadable or malformed one reads as empty.
func (*State) Liveness ¶
func (s *State) Liveness() map[string]LivenessRecord
Liveness is the liveness clocks, SANITIZED on read so corruption can never fail open (#1829 local P2-2): a non-object file reads as fresh, a non-object record as reset, and only well-typed fields survive — a non-negative integer idleSince, a non-empty handle, an integer frontier within the seq ceiling. Corruption costs at most one repeated nudge, never a suppressed worker.
func (*State) SaveLedger ¶
SaveLedger persists l atomically (0600).
type StatusReport ¶
type StatusReport struct {
StateDir string `json:"stateDir"`
TokenAdopted bool `json:"tokenAdopted"`
NudgedUnderCurrentToken []string `json:"nudgedUnderCurrentToken"`
Held map[string]string `json:"held"`
Locked bool `json:"locked"`
Roster []string `json:"roster"`
Liveness map[string]*LivenessStatus `json:"liveness"`
}
StatusReport is `team router status`: local state only — token presence, ledger contents, the lock, the liveness clocks. No network, and never the token itself.
func Status ¶
func Status(c *Config) (*StatusReport, error)
Status reads the router's local state for c.
type Terminal ¶
type Terminal struct {
Handle string `json:"handle"`
WorktreePath string `json:"worktreePath"`
AgentIdentity *string `json:"agentIdentity"`
Connected bool `json:"connected"`
Writable bool `json:"writable"`
LastOutputAt *int64 `json:"lastOutputAt"` // nil = untracked (#7398)
}
Terminal is one `orca terminal list` row.
func (*Terminal) UnmarshalJSON ¶
UnmarshalJSON reads Orca's row with router.ts's defaults: connected and writable count unless explicitly false, and ANY non-null lastOutputAt is tracking (only null means it was lost).