teamrouter

package
v0.30.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 22 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func BreakStaleLock

func BreakStaleLock(lockDir, staleOwner string) (string, error)

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

func DescribeOutcome(o *Outcome) string

DescribeOutcome is the one-line summary of a poll; never a token or a message body.

func LivenessNudgeText

func LivenessNudgeText(cursor, head int) string

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

func PromptLineIsEmpty(lines []string, family string) bool

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 RecordReload(c *Config) error

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

func ScreenAgentFamily(sc *Screen) string

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

type AttentionRequest struct {
	Since string
	Page  string
}

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

func LoadConfig(path string) (*Config, error)

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

type CursorResult struct {
	LastSeen  *int
	FieldErrs []string
}

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

func (s *GraphQLServer) ChannelHead(ctx context.Context, channelRef string) (int, error)

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

type OrcaRunner func(ctx context.Context, argv []string) RunResult

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

type ReloadReceipt struct {
	Fingerprint string    `json:"fingerprint"`
	At          time.Time `json:"at"`
}

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 RunResult

type RunResult struct {
	Code   int
	Stdout string
	Stderr string
}

RunResult is one exec'd `orca` invocation's outcome.

type Screen

type Screen struct {
	Source string
	Tail   []string
	Draft  string
}

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 NewState

func NewState(dir string) *State

NewState addresses the state directory dir; nothing is touched until used.

func (*State) Ledger

func (s *State) Ledger() 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

func (s *State) SaveLedger(l Ledger) error

SaveLedger persists l atomically (0600).

func (*State) Token

func (s *State) Token() string

Token is the adopted token, or "" when none is adopted or it is unreadable.

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

func (t *Terminal) UnmarshalJSON(b []byte) error

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

Jump to

Keyboard shortcuts

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