Documentation
¶
Overview ¶
Package delivery is one batch's deliver-and-confirm transaction (cli#907, C1), shared by the host poller and C2's posting CLI (#908). The server decides everything that needs no screen: recipients, filtering, rendering, batching and timing (S1, hadron-server#2017). This package decides only what the terminal shows: whether to type, and whether the text arrived.
It talks to the server through the Server seam. The GraphQL adapter arrives with S1/S5's schema on server main; nothing here depends on it.
Index ¶
- Constants
- Variables
- func BoundWorker(worktree string) string
- func CheckBatch(b Batch) error
- func CheckWake(w Wake) error
- func ClearKeys(leftover string) string
- func KnownCode(code string) bool
- func RenderWake(reasons []WakeReason) string
- func WithWorkerLock(dir, workerID string, fn func(stillOwner func() bool) error) error
- type AckResult
- type Batch
- type Capabilities
- type Cover
- type Deliverer
- type Event
- type Kind
- type Outcome
- type Range
- type Recipient
- type Record
- type Records
- func (s *Records) Delete(workerID string, ranges []Range) error
- func (s *Records) List(workerID string) ([]Record, error)
- func (s *Records) Prune(workerID string, cutoff time.Time) error
- func (s *Records) Put(b Batch, at time.Time) error
- func (s *Records) Retire(workerID string, rec Record, acked []Range) error
- type Server
- type Status
- type StatusBook
- type TaskReason
- type Terminals
- type Through
- type Wake
- type WakeKind
- type WakeRead
- type WakeReason
- type WakeRecord
- type WakeRecords
- func (s *WakeRecords) Acked(workerID string, keys []string, at time.Time) error
- func (s *WakeRecords) List(workerID string) (map[string]WakeRecord, error)
- func (s *WakeRecords) Prune(workerID string, cutoff time.Time) error
- func (s *WakeRecords) Typed(workerID string, reasons []WakeReason, at time.Time) error
- type WakeServer
- type WorkerStatus
Constants ¶
const ( HoldDraft = "draft" HoldIsolated = "isolated" HoldQuestion = "question" HoldScreenUnknown = "screen-unknown" HoldNotSubmitted = "not-submitted" HoldTwoAgents = "two-agents" HoldBindingMismatch = "binding-mismatch" )
Hold reasons: S5's closed vocabulary (hadron-server#2021).
const ( StepRead = "read" StepReadFailed = "read-failed" StepOffByScope = "off-by-scope" // WakeBudgetExhausted: the wake read answered WAKE_BUDGET_EXHAUSTED. WakeBudgetExhausted = "budget-exhausted" )
How a poller step's batch read went (cli#906): the poller reports one per step, so `team host status` can tell a quiet poller from a stopped one.
const ( TickPrefix = "[hadron tick]" TaskPrefix = "[hadron task]" )
Own prefixes of a typed wake line. With Prefix they are what a deliverer recognizes as its own leftover in a composer.
const ( WakeTyped = "typed" WakeAcked = "acked" )
Wake record states (cli#906 6257108208 §1). The two are kept apart: a typed record is pending evidence and never expires by age; only its own landed ack, or a newer tick of the same ticker whose ack landed, moves it to acked. Acked records are archive, pruned after a day.
const AckTimeout = 30 * time.Second
AckTimeout bounds one acknowledgement or hold report, which is sent even while stopping.
const DefaultBudget = 2*orcaterm.CallTimeout + AckTimeout + 30*time.Second
DefaultBudget is the expiry budget: everything after the screen checks must fit inside the proof's remaining life — a send with --wait-submit and a confirm read (one orca call bound each), the record write, and the ack — plus slack. A proof with less left is re-read, not typed on. This narrows the stale-proof window; it does not close it (network delay, a slow server), and a stale ack after a confirmed send is reconciled from the record, never re-typed.
const ( // is busy or unproven and it gave no busy deadline. HoldTimingUnavailable = "timing-unavailable" )
Hold reasons local to a wake. A wake hold is logged and shown in status, never reported to S5 (S5's hold is a batch event).
const MaxTextRunes = 65_536 + 4_096
MaxTextRunes bounds a batch's text by S1's own rule (renderBatch.ts at server main ad7f1c2): at most 10 messages within a 6,000-character body budget, except that the first message is always delivered whole and alone, up to the 65,536-character chat body. The header, authors, parent context, overflow lines and reply hint come on top; 4,096 covers them. A longer text is a contract error. The server counts UTF-16 units, which are never fewer than runes. (Colette's LOCAL P2 on #939: the old 8,000 refused a valid whole message.)
const MaxWakeRunes = 1000
MaxWakeRunes bounds a wake line. A longer one is cut at a reason boundary and says how many reasons it left out.
const Prefix = "[hadron chat"
Prefix opens every rendered batch (D8). It is how a deliverer recognizes its own leftover in a composer: anything else there is someone's draft.
const RecordConfirmed = "confirmed"
RecordConfirmed is the only state a record is ever written in. Nothing is persisted before the delivery test succeeds: a batch read, prepared or sent without confirmation has no record, and counts as not delivered.
Variables ¶
var ErrBusy = errors.New("another deliverer holds this worker's lock")
ErrBusy is a lock another live deliverer holds. A deliverer that finds it skips the worker this time and never waits: a post must not stall behind the poller.
ErrHoldUnavailable is Hold's answer when the server has no hold report (S5 not on it yet: cli#940). The adapter returns it without a call. The hold stays local: Held and unreported, never Failed, and never cached as reported, so it is reported once the capability is there.
var ErrMalformed = errors.New("malformed batch")
ErrMalformed is a batch that breaks S1's contract. It is neither typed nor acknowledged nor held (S5 has no reason for it, by design): it is logged by span and left due.
var ErrMalformedWake = errors.New("malformed wake")
ErrMalformedWake is a wake that breaks dueWakes' contract: neither typed nor acknowledged, logged by span and left due.
var OwnPrefixes = []string{Prefix, TickPrefix, TaskPrefix}
OwnPrefixes are every prefix C1 types: a leftover opening with any of them is ours (submitted or cleared), never a foreign draft.
Functions ¶
func BoundWorker ¶
BoundWorker is the worker id a worktree's session binding names, or "".
func CheckBatch ¶
CheckBatch is the shape a batch must have before anything is typed: one line (a newline would submit early), our prefix, a bounded length, and ranges whose seqs lie inside them.
func CheckWake ¶
CheckWake is the shape a wake must have before anything is typed: a proof, a worker, and reasons whose keys are complete.
func ClearKeys ¶
ClearKeys empties a composer holding leftover: one Backspace per rune. It does nothing to an empty composer, so a human who empties it between our read and our send loses nothing. Never Ctrl+C: ONE on an empty Codex composer quits Codex. Never Ctrl+U (one line only) or Esc Esc (Claude opens its Rewind dialog on an empty composer).
func RenderWake ¶
func RenderWake(reasons []WakeReason) string
RenderWake is the one line typed for reasons (TICK and TASK only). Server-free and count-free, so it holds for a tick with no channels and for every-tick: reasons joined by "; ", cut at a reason boundary to MaxWakeRunes with the count of reasons left out.
func WithWorkerLock ¶
WithWorkerLock runs fn holding workerID's lock under dir. fn receives stillOwner, re-checked before every send and every write, so a deliverer that somehow lost the lock stops instead of acting alongside another.
Types ¶
type Batch ¶
type Batch struct {
// ID is S1's signed proof. It is sent back to ack or hold and nowhere
// else: never logged, stored or put in argv.
ID string
// BatchKey is S1's stable hash over the ranges: the same messages, the
// same key, across re-issued proofs. A lookup key, never evidence.
BatchKey string
WorkerID string
Ranges []Range
Text string
// DeliverAfter has passed for every batch S1 returns; it is checked
// anyway. BusyDeadline is DeliverAfter plus S9's busy-delay cap: a
// working agent defers a batch until then and no later.
DeliverAfter time.Time
BusyDeadline time.Time
ProofExpiresAt time.Time
// Interrupt is set only for a halt (S6, phase 4): always false now.
Interrupt bool
}
Batch is one ready-to-deliver batch as S1 renders it.
type Capabilities ¶
type Capabilities struct {
// EnterOnly: an Enter-only send submits what the composer holds.
EnterOnly map[string]bool
// Clear: ClearKeys empties the family's composer.
Clear map[string]bool
}
Capabilities are what has been PROVEN on a scratch terminal, per TUI family. Until proven, the path it would enable holds instead.
func Proven ¶
func Proven() Capabilities
Proven is what the 2026-10-18 scratch terminal proved on Claude Code 2.1.303 and Codex CLI 0.163.0 (cli#907; hadron-dev-team-joshua:work: cli907-scratch-terminal-evidence-2026-10-18): Enter alone submits an existing composer, and ClearKeys empties one, multi-line included.
type Cover ¶
type Cover struct {
// Full: every message of the batch was confirmed delivered. Ack it whole,
// with no typing.
Full bool
// Through: the channels with a confirmed covered prefix, when not Full.
// Channels left out are not covered and move nothing.
Through []Through
// Matched are the records that proved it.
Matched []Record
}
Cover is what confirmed records prove about a batch.
func Reconcile ¶
Reconcile is what confirmed records prove about b. A record applies to a channel only when the channel id AND the incarnation match: a recreated channel restarts its seqs on the same id (hadron-server src/lib/channels.ts at b8384631), so an earlier incarnation's record proves nothing. Within a channel the covered prefix is the batch's own seqs from its first, for as long as each is in one record's confirmed seqs. The first seq not in the record ends it, so a later distinct message is never acked unseen, and a batch whose first seq the record lacks gets no ack at all. (cli#907 delta 2 also asked that the record START at the batch's first seq. That only ever refused a record starting earlier, e.g. [4,5,6] against [5,6,7], which proves 5 and 6 confirmed: refusing it would retype them.) A batchKey match with a confirmed record is a full cover by S1's definition of the key.
type Deliverer ¶
type Deliverer struct {
Server Server
Orca Terminals
Records *Records
// LockDir holds the per-worker locks shared with C2.
LockDir string
Caps Capabilities
// Bound is the worker a worktree's session binding names, or "".
Bound func(worktree string) string
Now func() time.Time
Budget time.Duration
Emit func(Event)
// Wakes acknowledges wake proofs; nil leaves wakes unacknowledged.
Wakes WakeServer
WakeRecords *WakeRecords
// AckMixed is set only once A′ (hadron-server#2072) is verified
// deployed: a wake carrying a MENTION, or naming channels, may then be
// acknowledged once all its TICK and TASK reasons are typed. Off, C1
// acknowledges only a proof with no MENTION reason and no channels.
AckMixed bool
// contains filtered or unexported fields
}
Deliverer delivers batches into Orca terminals.
func (*Deliverer) Deliver ¶
Deliver carries one batch to its recipient's terminal, under the worker's lock. The order is the design (cli#907 attach §2 and deltas 2–4): reconcile confirmed records first (never type what was confirmed), check the batch and the proof's remaining life, find exactly one agent terminal in a registered worktree with a matching binding, judge its composer under R2 and its liveness, then send, confirm, record, and ack.
func (*Deliverer) DeliverWake ¶
DeliverWake types w's not-yet-recorded TICK and TASK reasons into r's terminal as one line, records them, and acknowledges the proof when the accepted set allows it.
The order mirrors a batch's: reconcile records first (a reason already typed is never typed again), then the shared terminal path, then record every typed key before the ack. A proof is acknowledged only when ackable.
type Event ¶
type Event struct {
Time time.Time
Worker string
WorkerID string
Span string
Kind Kind
Reason string
Terminal string
// ResolvedBy says where the recipient terminal's agent family came
// from: "identity" (Orca's agent identity) or "screen" (Orca reported
// none, and the screen showed one). Empty before a terminal is found. A
// diagnostic code only (cli#906): it grants nothing.
ResolvedBy string
// Wake: the event is a tick or task wake's (cli#906 row 1), not a
// batch's. Span is then reason counts.
Wake bool
// Unreported: a hold the server was not told about (cli#940).
Unreported bool
}
Event is one line of what delivery did, for the operator.
type Kind ¶
type Kind string
Kind is a delivery's outcome.
const ( Delivered Kind = "delivered" // typed and confirmed Reconciled Kind = "reconciled" // acknowledged from a confirmed record, nothing typed Held Kind = "held" // left due with a reason; reported to S5 unless Unreported (#940) Deferred Kind = "deferred" // left due: try again next time Skipped Kind = "skipped" // left due, without a hold (no terminal, lock busy) Failed Kind = "error" // a contract or transport error; left due )
type Outcome ¶
type Outcome struct {
Kind Kind
Reason string
Terminal string
// AckPending: delivered and recorded, but the ack did not land; the
// record reconciles it next time without typing.
AckPending bool
// Unreported: Held, but the server was not told (ErrHoldUnavailable).
// No server hold, episode, alarm or escalation exists for it.
Unreported bool
}
Outcome is what one Deliver did. Reason is a code; nothing here carries text, a proof or screen content.
type Range ¶
type Range struct {
ChannelID string `json:"channelId"`
Incarnation int `json:"incarnation"`
FromSeq int `json:"fromSeq"`
ThroughSeq int `json:"throughSeq"`
Seqs []int `json:"seqs"`
}
Range is one channel's part of a batch (S1 #2017 issuecomment-6246620593). Seqs are the exact message seqs rendered, at most ten across the batch.
type Recipient ¶
type Recipient struct {
WorkerID string
Name string
// TUI is the agent interface its terminal must show ("claude" or
// "codex"), when the host knows it. Empty means the screen's own family
// and Orca's agent identity must agree instead.
TUI string
// Worktrees are its registered worktrees, absolute.
Worktrees []string
// contains filtered or unexported fields
}
Recipient is a worker registered on this host, as the poller's latest registration resolved it.
type Record ¶
type Record struct {
State string `json:"state"`
WorkerID string `json:"workerId"`
BatchKey string `json:"batchKey,omitempty"`
Ranges []Range `json:"ranges"`
ConfirmedAt time.Time `json:"confirmedAt"`
}
Record is submitted evidence for one batch: written after a confirmed send and before its ack, deleted once the ack lands. It holds no text, no proof and no batch id.
type Records ¶
type Records struct {
Dir string
}
Records is the confirmed-delivery store: one 0600 file per batch under a 0700 directory per worker. Files are written by atomic rename and named by a hash of the ranges, so a writer that lost its lock never clobbers another deliverer's record.
func (*Records) List ¶
List is the worker's confirmed records. A file that does not parse as a confirmed record for this worker is ignored: only typed, confirmed evidence may suppress a send.
func (*Records) Prune ¶
Prune drops records confirmed before cutoff: past the proof's lifetime a record can only match a re-hand, and S1 re-hands within minutes.
func (*Records) Retire ¶
Retire removes from rec the messages an ack just covered (acked, the acknowledged batch's ranges) and keeps the rest: a record can confirm seqs or channels the acknowledged batch did not carry, and those are still owed their own ack (Jane's P2 on #939). The remainder keeps rec's confirmation time and drops its batchKey, which named the whole. It is written before rec is removed, so a crash between leaves both, which only proves the same messages twice. A record with nothing left is deleted.
type Server ¶
type Server interface {
Ack(ctx context.Context, batchID string, through []Through) (AckResult, error)
Hold(ctx context.Context, batchID, reason, terminalID string) error
}
Server is S1's acknowledgement and S5's hold, as delivery consumes them. An Ack error of any kind (a refusal included: BAD_USER_INPUT, WAKE_PROOF_STALE) means nothing moved, so a confirmed record is kept.
type Status ¶
type Status struct {
Version int `json:"version"`
LastStepAt time.Time `json:"lastStepAt"`
LastRead string `json:"lastRead"`
Workers map[string]WorkerStatus `json:"workers"`
// The newest wake read (cli#906 row 1): its code, and dueWakes' counts.
WakeRead string `json:"wakeRead,omitempty"`
BudgetExhaustedAt *time.Time `json:"budgetExhaustedAt,omitempty"`
Dormant int `json:"dormant"`
Withheld int `json:"withheld"`
}
Status is the poller's local delivery snapshot. It is observational: what this host's poller last saw, never authoritative delivery health (that is S5's). It holds no text, proof, draft or batch id.
func ReadStatus ¶
ReadStatus reads the snapshot under stateDir. A missing file is an fs.ErrNotExist error; a corrupt one is any other error.
type StatusBook ¶
type StatusBook struct {
// contains filtered or unexported fields
}
StatusBook keeps the snapshot as events arrive and writes it after each poller step. Safe for concurrent use: deliveries for different workers run at once.
func NewStatusBook ¶
func NewStatusBook(stateDir string) *StatusBook
NewStatusBook keeps the snapshot under stateDir, beside the records and locks.
func (*StatusBook) Observe ¶
func (b *StatusBook) Observe(e Event)
Observe records e as its worker's newest event, unless an event already recorded is newer. It writes nothing: Step does.
func (*StatusBook) ObserveWakes ¶
func (b *StatusBook) ObserveWakes(r WakeRead)
ObserveWakes records a step's wake read. It writes nothing: Step does. The budget time is kept only while the newest read says it is spent.
type TaskReason ¶
type TaskReason struct {
EventID string
ChainIndex int
// Event is the line's wording: "assigned", "child finished", "alarm" or
// "end session".
Event string
// ID and Title are the task's. A task-less alarm (S5 #2021
// comment-6259060238: taskId null) has neither, and none is invented.
ID string
Title string
// AlarmType is a task-less alarm's closed type code from the server
// (e.g. delivery.stuck), shown as given.
AlarmType string
}
TaskReason is the task a TASK reason is for (DueTaskReason).
type Terminals ¶
type Terminals interface {
List(ctx context.Context) ([]orcaterm.Terminal, error)
ReadScreen(ctx context.Context, handle string) *orcaterm.Screen
Send(ctx context.Context, handle, text string, o orcaterm.SendOptions) orcaterm.Receipt
}
Terminals is the orca surface delivery uses; *orcaterm.Client is one.
type Through ¶
Through limits an ack to a channel's covered prefix: Seq must be one of that channel's rendered seqs (S1), and the mark moves from the range's start to Seq and no further. Channels left out move nothing.
type Wake ¶
type Wake struct {
// Proof is the signed proof handed back to acknowledgeWakes. Never
// logged, stored or put in argv.
Proof string
WorkerID string
Reasons []WakeReason
// NamesChannels: the proof carries channel ranges (DueWake.channels is
// non-empty). Before A′ is deployed, acknowledging such a proof could
// mark unseen text delivered, so C1 never does.
NamesChannels bool
// DeliverAfter and BusyDeadline are S1 slice (c)'s timing (R63): the
// busy deadline counts from the oldest waiting reason's due time. A zero
// one means the server did not say, and C1 invents none: no
// DeliverAfter holds the wake as timing-unavailable even for an idle
// agent, and no BusyDeadline holds it so for a busy or unproven one.
DeliverAfter time.Time
BusyDeadline time.Time
}
Wake is one due wake as dueWakes returns it.
type WakeRead ¶
WakeRead is how a step's wake read went (cli#906 row 1): its code and the counts dueWakes reports for status. Counts only.
type WakeReason ¶
type WakeReason struct {
Kind WakeKind
TickerID string
TickerName string
// LastTickAt names the tick (k) a TICK reason is for: it changes per
// tick, so with the ticker it is the reason's stable identity.
LastTickAt time.Time
Task *TaskReason
}
WakeReason is one reason a wake is due.
func (WakeReason) Key ¶
func (r WakeReason) Key() string
Key is the reason's stable submitted identity: tick:<tickerId>:<lastTickAt> or task:<eventId>:<chainIndex>. "" for a MENTION, which C1 never records.
type WakeRecord ¶
type WakeRecord struct {
State string `json:"state"`
WorkerID string `json:"workerId"`
Key string `json:"key"`
TickerID string `json:"tickerId,omitempty"`
TickAt time.Time `json:"tickAt,omitzero"`
At time.Time `json:"at"`
}
WakeRecord is submitted evidence for one TICK or TASK reason. It holds the reason's key and nothing else from the wake: no text, no proof.
type WakeRecords ¶
type WakeRecords struct {
Dir string
}
WakeRecords is the wake-reason store: one 0600 file per key under a 0700 directory per worker, written by atomic rename.
func (*WakeRecords) Acked ¶
Acked moves records to acked. A key with no record is skipped: only typed evidence is ever archived.
func (*WakeRecords) List ¶
func (s *WakeRecords) List(workerID string) (map[string]WakeRecord, error)
List is the worker's wake records, keyed by reason key. A file that does not parse as this worker's record under its own name is ignored.
func (*WakeRecords) Prune ¶
func (s *WakeRecords) Prune(workerID string, cutoff time.Time) error
Prune drops acked records older than cutoff. Typed records are never pruned by age.
func (*WakeRecords) Typed ¶
func (s *WakeRecords) Typed(workerID string, reasons []WakeReason, at time.Time) error
Typed records reasons whose line was confirmed submitted. Written after the confirmation and before any ack.
type WakeServer ¶
WakeServer is acknowledgeWakes, for one proof C1 itself read. An error of any kind means nothing moved.
type WorkerStatus ¶
type WorkerStatus struct {
Name string `json:"name"`
Kind Kind `json:"kind"`
Reason string `json:"reason,omitempty"`
At time.Time `json:"at"`
ResolvedBy string `json:"resolvedBy,omitempty"`
// Wake: the event was a tick or task wake's, not a batch's.
Wake bool `json:"wake,omitempty"`
// Unreported: the newest event was a hold the server was not told about.
Unreported bool `json:"unreported,omitempty"`
}
WorkerStatus is a worker's newest delivery event: codes only.