Documentation
¶
Overview ¶
Package sessiontransport defines the one Go transport interface WB's session layer addresses a terminal through (REQ:single-transport-interface), the typed capability matrix each implementation declares (REQ:transport-capability-matrix), and the capability-driven guard that decides record-only vs. advisory vs. submit delivery for every event class, daemon-originated or the pre-existing successor-messaging path alike (REQ:delivery-guards-enforced-and-rechecked). See spec/features/herdr-session-transport/README.md and spec/plans/herdr-session-transport.md (Task 2) for the Feature and Plan this package implements.
What this package ships ¶
Transport is the interface every session code path that addresses a terminal must go through: Transport.ResolvePane and Transport.Launch find or start a live Target, Transport.Inspect polls its liveness, [Transport.MatchesReceiptIdentity] confirms a receipt-carried name against the transport's own naming convention, and Transport.Deliver sends text at whatever DeliveryMode its own Capabilities and the delivery's EventClass resolve to — recomputed inside Deliver itself, never trusted from a caller. Capabilities is the typed matrix an implementation declares, including Capabilities.LineageSubmit, the pre-existing successor-messaging submit path's own capability, distinct from the daemon-wake capabilities. NoneTransport is the first shipped implementation (REQ:none-transport-is-first-class). ResolveDeliveryMode is the guard every delivery path calls before touching a pane, encoding two rules: absence of empty-input evidence is itself a guard failure for the daemon's own-PR-outcome class, never grounds to submit (REQ:delivery-guards-enforced-and-rechecked); and LineageSubmit is consulted only for EventSuccessorMessage, never for a daemon-originated class, so a transport claiming it (tmux) still never delivers a daemon event unguarded (AC:tmux-never-delivers-unguarded). ResolveOverride and LoadOverride are the explicit `wb.yaml` `session.transport` / command-flag override and its fail-closed validation (REQ:explicit-transport-override), applying a default per-Kind prerequisite check even when the caller passes no check of its own. internal/sessiontransport/transporttest.Suite is the shared contract test both remaining implementations must pass (REQ:two-transport-implementations); it lives in its own package so this one never carries a "testing" dependency into production code.
What this package does not ship ¶
No production call site is rewired onto Transport yet. Task 3 moves today's tmux code (internal/sessionlaunch, internal/sessionmessage, and related packages) behind this interface with no behavior change; Task 4 wires the herdr implementation onto internal/herdr (Task 1, already landed). Automatic transport selection (REQ:automatic-transport-selection), identity capture (REQ:automatic-transport-identity-capture), and persisting the resolved Kind on a session record (REQ:selected-transport-recorded) are Task 5's. The delivery-intent coalescing store, the watcher, and the facts-only templates are Tasks 6-9's. This package carries no delivery policy beyond the capability-driven mode decision itself: no message templates, no watcher, no persistence.
Capability declarations are documentation, not discovery ¶
HerdrCapabilities and TmuxCapabilities describe what Task 4's and Task 3's implementations will declare once built. This package states them now so ResolveDeliveryMode and the contract suite have something concrete to prove the mechanism against ahead of that wiring — see AC:tmux-never-delivers-unguarded and AC:none-transport-records-only, which this package's own tests exercise directly. Once Task 3 or Task 4 lands, its own Transport.Capabilities method is the live source of truth, not these package-level functions; a future change to one should be paired with the implementation it documents. They are functions, not exported vars, so nothing outside this package can corrupt the shared declaration for the rest of the process.
Index ¶
- Variables
- func DeliveryModeExceeds(got, allowed DeliveryMode) bool
- func MatchesReceiptIdentity(transport Transport, successorWBSessionID, name string) bool
- type Capabilities
- type Delivery
- type DeliveryMode
- type EventClass
- type Identity
- type Inspection
- type Kind
- type LaunchRequest
- type LookPath
- type NoneTransport
- func (NoneTransport) AddressFor(string) string
- func (NoneTransport) Capabilities() Capabilities
- func (NoneTransport) Deliver(_ context.Context, target Target, delivery Delivery) (Receipt, error)
- func (NoneTransport) Inspect(context.Context, Target) (Inspection, error)
- func (NoneTransport) Kind() Kind
- func (NoneTransport) Launch(context.Context, LaunchRequest) (Target, error)
- func (NoneTransport) ResolvePane(context.Context, Identity) (Target, error)
- type Operation
- type OverrideConfig
- type PrerequisiteCheck
- type Receipt
- type Target
- type Transport
Constants ¶
This section is empty.
Variables ¶
var ErrNoUniqueTarget = errors.New("sessiontransport: no unique live target for this identity")
ErrNoUniqueTarget means Transport.ResolvePane found zero, or more than one, live candidate for the given Identity — herdr's zero-or-multiple `agent list` match (REQ:pane-resolved-by-session-identity-match), or tmux's `list-panes` returning other than exactly one line (internal/sessionmessage/tmux.go's Inspect: "want exactly one").
Scope: this governs daemon-wake resolution (Task 7's EventOwnPROutcome/EventAdvisory delivery). A daemon-wake caller MUST treat it exactly like a no-live-owner outcome — resolve to record-only, never guess among candidates, and never fall back to a stale previously recorded target (AC:pane-resolved-uniquely-or-record-only).
It does NOT retroactively relax `wb session receive-message`'s pre-existing successor-messaging behavior (REQ:existing-successor-messaging-binding-path, internal/sessionmessage/receive.go): today, a pane-count mismatch or a paste failure there is a hard error, exactly as REQ:tmux-parity-preserved requires, because today's MessageReceipt schema has no way to express a non-fatal "recorded, not delivered" outcome. Only once Task 4's `delivery: recorded` receipt state (REQ:recorded-only-receipt-state) exists does an EventSuccessorMessage resolution failure stop being a hard error and start following this same record-only rule.
var ErrOverrideInvalid = errors.New("sessiontransport: explicit transport override does not name a shipped transport")
ErrOverrideInvalid means an explicit override named a value that is not one of the transports WB actually ships (REQ:explicit-transport-override).
ErrOverridePrerequisiteUnavailable means an explicit override named a shipped transport whose runtime prerequisite is not reachable right now. REQ:explicit-transport-override requires WB to refuse on this error, never to silently fall back to automatic selection.
Functions ¶
func DeliveryModeExceeds ¶
func DeliveryModeExceeds(got, allowed DeliveryMode) bool
DeliveryModeExceeds reports whether got is a strictly more binding delivery mode than allowed — submit exceeds advisory and record-only; advisory exceeds record-only; nothing exceeds submit. It is exported so a contract test (internal/sessiontransport/transporttest) can assert that a real Transport.Deliver outcome never exceeds what ResolveDeliveryMode computed, while still permitting a transport to downgrade further for its own additional reasons — a zero Target, most importantly, which has nowhere to advise or submit into regardless of what the capability matrix alone would otherwise allow.
func MatchesReceiptIdentity ¶
MatchesReceiptIdentity reports whether name — a receipt-carried address — is the exact address transport would use for successorWBSessionID: name == transport.AddressFor(successorWBSessionID). It is a package-level helper, not a Transport method, because Transport.AddressFor alone already carries every transport-specific fact this comparison needs; every implementation would otherwise repeat the identical one-line body.
Types ¶
type Capabilities ¶
type Capabilities struct {
// Kind names which shipped transport this matrix describes. It exists
// so a Capabilities value carries its own identity when logged or
// compared in a test, never so a caller branches on it directly —
// REQ:single-transport-interface reserves that judgement for transport
// selection, not for call sites above [Transport].
Kind Kind
// LiveStatus reports whether this transport can ask, right now, whether
// the owning session is idle, working, blocked, or done. herdr's
// `agent list`/`agent get` supply this; tmux structurally cannot.
LiveStatus bool
// EmptyInputEvidence reports whether this transport can produce
// positive evidence that a pane's input box is empty. No shipped
// transport declares this true today: the mechanism is Deferred in the
// Feature (see "Empty-input evidence mechanism"), so
// REQ:delivery-guards-enforced-and-rechecked's guard can never be
// satisfied yet — by construction, not by circumstance.
EmptyInputEvidence bool
// AdvisoryDelivery reports whether this transport can paste unsubmitted
// text into a live pane (herdr's `pane send-text`) for a
// daemon-originated event outside the session's own-PR-outcome class.
// tmux declares this false by default
// (REQ:tmux-record-only-for-daemon-events): it has no live status and
// no input evidence at all, so there is nothing to guard even an
// advisory paste against a mid-typing human. This field governs only
// daemon-originated delivery ([EventOwnPROutcome], [EventAdvisory]); it
// has no bearing on [LineageSubmit].
AdvisoryDelivery bool
// LineageSubmit reports whether this transport submits the pre-existing
// successor-messaging path — `wb session send`'s and `recall`'s
// target-side write ([EventSuccessorMessage],
// REQ:existing-successor-messaging-binding-path) — guarded by that
// receipt's lineage rather than by the daemon wake's own-PR-outcome
// restriction. tmux declares this true: its paste-buffer payload
// already ends in a newline today
// (internal/sessionmove/types.go:622's marshalJSON) and
// REQ:tmux-parity-preserved keeps that unchanged. herdr and none
// declare it false: no herdr submit path is built in this iteration
// (Deferred: "Successor messaging's herdr submit path"), and none never
// submits anything. LineageSubmit is never consulted for
// [EventOwnPROutcome] or [EventAdvisory] — only [EventSuccessorMessage]
// — so a transport declaring it true still never submits a
// daemon-originated event (AC:tmux-never-delivers-unguarded's guarantee
// extends to this field too).
LineageSubmit bool
// PaneEnumeration reports whether this transport can enumerate live
// panes/agents to resolve a session's identity to a unique target
// (REQ:pane-resolved-by-session-identity-match). Resolving to zero or
// to more than one candidate is never treated as a target — see
// [Transport.ResolvePane] and [ErrNoUniqueTarget].
PaneEnumeration bool
}
Capabilities is the typed capability matrix REQ:transport-capability-matrix requires: what a resolved transport can prove about a live session, never what it might be able to approximate. Every field defaults false, so a transport that declares nothing gets the safest outcome — never submit, never advise, always record — from ResolveDeliveryMode by construction.
func HerdrCapabilities ¶
func HerdrCapabilities() Capabilities
HerdrCapabilities returns the capability matrix Task 4's herdr transport declares. Task 2 states it here as the documented contract Task 4 implements against; Task 4 owns actually wiring the herdr transport. It is a function, not an exported var, so no caller can corrupt the shared declaration by mutating what it got back (a struct assignment already copies by value, but an exported var remains directly assignable from any importer — a function returning a fresh value each call closes that off entirely).
func NoneCapabilities ¶
func NoneCapabilities() Capabilities
NoneCapabilities returns the capability matrix NoneTransport declares (REQ:none-transport-is-first-class): nothing is live, so every event — daemon-originated or the pre-existing successor-messaging path alike — resolves record-only (AC:none-transport-records-only).
func TmuxCapabilities ¶
func TmuxCapabilities() Capabilities
TmuxCapabilities returns the capability matrix Task 3's tmux transport declares by default (REQ:tmux-record-only-for-daemon-events): no live status, no empty-input evidence, and — the lead's default, not a fixed founder decision (see the Feature's Open Questions) — no advisory delivery either, so every daemon-originated event resolves record-only on tmux (AC:tmux-never-delivers-unguarded). LineageSubmit is true: tmux's pre-existing successor-messaging paste already submits today and keeps doing so unchanged (REQ:tmux-parity-preserved) — a fact about a different, differently guarded path (REQ:existing-successor-messaging-binding-path), not a relaxation of the daemon-wake guarantee above.
type Delivery ¶
type Delivery struct {
Operation Operation
Class EventClass
// Key is the idempotency key a keyed delivery mechanism needs — tmux's
// buffer name is `wb-message-<Key>` (internal/sessionmessage/receive.go:189,
// asserted by receive_test.go:97). Required for [OperationMessage];
// [OperationWake] leaves it empty.
Key string
// Text is the exact bytes to deliver.
// REQ:facts-only-templates governs what Text may ever contain for a
// daemon-originated event; this package does not compose message text,
// it only carries whatever the caller already built.
Text string
}
Delivery is one Transport.Deliver call's payload: which session-layer Operation this is, which EventClass governs it, and the text (and, for a keyed mechanism, the Key) to carry. There is deliberately no Mode field — Transport.Deliver recomputes the mode itself from its own Transport.Capabilities and Class, exactly as ResolveDeliveryMode would, rather than trusting a caller-supplied mode that could be stale or wrong by the time Deliver actually runs.
type DeliveryMode ¶
type DeliveryMode string
DeliveryMode is what ResolveDeliveryMode decided, and what Transport.Deliver must honor exactly: ModeRecordOnly must never touch a live pane, ModeAdvisory must never submit, and ModeSubmit is reserved for EventSuccessorMessage on a Capabilities.LineageSubmit transport (today, only tmux) or for EventOwnPROutcome once the Deferred empty-input evidence mechanism lands (REQ:prompt-restricted-to-own-pr-outcome).
const ( // ModeRecordOnly means WB records the event and reports no live // delivery occurred; the calling command does not fail // (REQ:none-transport-is-first-class, REQ:no-live-owner-is-record-only). ModeRecordOnly DeliveryMode = "record_only" // ModeAdvisory means WB pastes unsubmitted text into a live pane // (REQ:advisory-mechanism-and-newline-boundary). ModeAdvisory DeliveryMode = "advisory" // ModeSubmit means WB submits text as if the session's own operator had // typed and pressed enter. For [EventOwnPROutcome], no shipped // [Capabilities] value ever causes [ResolveDeliveryMode] to return this // today (Deferred in the Feature). For [EventSuccessorMessage], tmux // does reach it, matching its pre-existing, unchanged behavior. ModeSubmit DeliveryMode = "submit" )
func ResolveDeliveryMode ¶
func ResolveDeliveryMode(caps Capabilities, operation Operation, class EventClass) DeliveryMode
ResolveDeliveryMode is the one seam every delivery path calls before doing anything to a pane (REQ:transport-capability-matrix, REQ:delivery-guards-enforced-and-rechecked). It only ever inspects the resolved transport's declared capability matrix, the Operation, and the event's class — never a specific transport Kind — so no call site branches on herdr-vs-tmux directly (REQ:single-transport-interface; Task 3's job is to prove no production call site does, this function only proves the mechanism itself does not need to). Transport.Deliver implementations call this themselves rather than trusting a caller-supplied mode; its result is an upper bound a transport MAY downgrade further (a zero Target, for instance — see Transport.Deliver) but MUST NOT exceed (DeliveryModeExceeds).
Operation matters because a caller can mislabel a Delivery: passing OperationWake with class EventSuccessorMessage, for instance, must never inherit EventSuccessorMessage's submit path — that path belongs only to OperationMessage. Each class therefore has exactly one Operation it is ever evaluated for; every other Operation forces ModeRecordOnly regardless of capabilities, before any capability is even consulted.
EventOwnPROutcome (only ever evaluated for OperationWake) can resolve ModeSubmit only when both LiveStatus and EmptyInputEvidence are true. No shipped Capabilities value declares EmptyInputEvidence true today, so this branch is unreachable in practice, by construction, not by circumstance — AC:guards-are-unconditional-and-block-submission. Absence of the evidence is itself a guard failure: it is never treated as grounds to advise instead, because REQ:prompt-restricted-to-own-pr-outcome reserves the own-PR-outcome class for submit-or-record only, never advisory.
EventSuccessorMessage (only ever evaluated for OperationMessage) can resolve ModeSubmit only when LineageSubmit is true — never because of LiveStatus, EmptyInputEvidence, or AdvisoryDelivery, which govern only the daemon-wake classes. A transport that declares LineageSubmit true (tmux) still never reaches ModeSubmit for EventOwnPROutcome or EventAdvisory: LineageSubmit is consulted in exactly one branch, this one, and only when Operation is OperationMessage (AC:tmux-never-delivers-unguarded's guarantee holds regardless of LineageSubmit, and regardless of a caller mislabeling Operation).
EventAdvisory (only ever evaluated for OperationWake) resolves ModeAdvisory only when AdvisoryDelivery is true, and ModeRecordOnly otherwise — which is why a Capabilities value with AdvisoryDelivery false (tmux's default, none's only value) always resolves ModeRecordOnly for it (AC:tmux-never-delivers-unguarded, AC:none-transport-records-only).
Any other, unrecognized, or empty EventClass resolves ModeRecordOnly: an unrecognized class is never grounds to advise or submit, only to record.
type EventClass ¶
type EventClass string
EventClass distinguishes the two event classes ever eligible for a submitted (binding) delivery — the daemon's own-PR-outcome class, and the pre-existing, differently guarded successor-messaging path — from every other, advisory-or-record-only event.
const ( // EventOwnPROutcome is the session's own registered pull request // settling — the only daemon-originated event class // REQ:prompt-restricted-to-own-pr-outcome ever names as submit-eligible. // It never actually resolves [ModeSubmit] in this iteration, because the // empty-input evidence guard can never be satisfied // (AC:guards-are-unconditional-and-block-submission). EventOwnPROutcome EventClass = "own_pr_outcome" // EventAdvisory is every other daemon-originated event class // (REQ:advisory-for-everything-else). It is never submit-eligible; it // resolves either [ModeAdvisory] or [ModeRecordOnly]. EventAdvisory EventClass = "advisory" // EventSuccessorMessage is the pre-existing successor-messaging submit // path — `wb session send`'s and `recall`'s target-side write // (REQ:existing-successor-messaging-binding-path) — guarded by that // receipt's lineage, never by REQ:prompt-restricted-to-own-pr-outcome's // own-PR-outcome restriction. It resolves [ModeSubmit] only when the // resolved transport declares [Capabilities.LineageSubmit]; otherwise // [ModeRecordOnly] (REQ:recorded-only-receipt-state: "recorded is not // herdr-specific: any transport... that cannot paste... produces it"). // It never resolves [ModeAdvisory]: this path has always been a durable // record-or-submit decision, never an unsubmitted paste. EventSuccessorMessage EventClass = "successor_message" )
type Identity ¶
type Identity struct {
Kind Kind
// WBSessionID is this session's own opaque WB identity (session.NewID).
// tmux's ResolvePane resolves this to a live pane by its deterministic
// session-name convention, "wb-session-" + WBSessionID
// (internal/sessionlaunch/launch.go:482); [Transport.MatchesReceiptIdentity]
// checks the identical convention against a receipt-carried name.
WBSessionID string
// HarnessSessionID is the harness's own session identifier —
// CLAUDE_CODE_SESSION_ID for Claude Code — the value herdr's
// ResolvePane must match uniquely against a live candidate's
// `agent_session` (REQ:pane-resolved-by-session-identity-match). Empty
// on tmux and none, which have no such live-status API to match
// against.
HarnessSessionID string
// Socket is the herdr socket path this identity's coordinates are
// scoped to (herdr's IDs are scoped to one server). Empty on tmux and
// none.
Socket string
}
Identity is what a session recorded about its terminal identity at registration time, re-walked at delivery time (REQ:resolution-at-delivery). Task 5 owns capturing and persisting this on the session record; this package only defines the shape Transport.ResolvePane consumes.
type Inspection ¶
type Inspection struct {
// Live reports whether the target's process is running right now. PID
// is meaningful only when Live is true.
Live bool
PID int
// Terminated reports whether the transport found terminal evidence — a
// dead-but-still-inspectable pane, on tmux — distinct from the target
// never having existed, or already having been cleaned up. ExitStatus
// and Diagnostic are meaningful only when Terminated is true.
Terminated bool
ExitStatus int
Diagnostic string
}
Inspection is what Transport.Inspect learns about a Target: whether it is live, and when it is not, whatever terminal evidence the transport can find. It replaces tmux's separate PanePID (live/PID probe) and PaneFailure (dead/exit-status/diagnostic probe) (internal/sessionlaunch/tmux.go) with the one question every poll loop actually asks: is it live, and if not, why not.
type Kind ¶
type Kind string
Kind names one of the transport implementations WB ships. It is the value REQ:selected-transport-recorded persists on a session record (Task 5) and the value ResolveOverride validates an explicit override against.
The transports WB ships. REQ:two-transport-implementations fixes the set at exactly herdr and tmux; KindNone is the first-class fallback REQ:none-transport-is-first-class requires when neither is observable.
func Kinds ¶
func Kinds() []Kind
Kinds returns every transport WB ships, in a fresh slice each call. ResolveOverride rejects any value outside this set; it carries no preference order — that belongs to automatic selection (REQ:automatic-transport-selection, Task 5's). It is a function, not an exported slice var, so no caller can corrupt the shipped set for the rest of the process by mutating what it got back.
func LoadOverride ¶
LoadOverride reads the optional `session.transport` override from configPath. An absent file, or a file with no `session` section at all, is not an error: it reports ("", false, nil) so a caller falls through to automatic selection (Task 5), distinguishing "no override configured" from "an override that failed to validate."
The `session` section itself is decoded strictly, with yaml.Decoder.KnownFields(true) — following internal/lifecyclehooks/config.go's pattern of parsing the whole file generically first, then re-decoding only the one section of interest under strict field checking — so a typo such as `transprot` fails closed with an error rather than silently decoding as "no override configured". Every other top-level section (`session_move`, `remote`, ...) is still tolerated untouched, because only the `session` node is ever extracted and re-decoded.
func ResolveOverride ¶
func ResolveOverride(requested Kind, check PrerequisiteCheck) (Kind, error)
ResolveOverride validates an explicit transport override (from `wb.yaml`'s `session.transport`, or an equivalent command flag) against the transports WB ships, and against whatever check reports about its runtime prerequisite. It fails closed on both: an unshipped Kind, or a shipped Kind whose prerequisite check fails, is refused outright, and the zero Kind is returned alongside the error so a caller cannot mistakenly treat a failed resolution as having chosen some other transport (AC:explicit-override-fails-closed's "does not silently fall back to herdr or none"). A nil check applies [defaultPrerequisiteCheck] rather than skipping validation entirely.
type LaunchRequest ¶
type LaunchRequest struct {
SuccessorWBSessionID string
// Cwd is the working directory the successor's process starts in.
Cwd string
// Executable is the absolute path to the process to start.
Executable string
// Args are the executable's own arguments, in order.
Args []string
}
LaunchRequest is what a successor launch needs to start a new terminal for a resumed or moved session. Cwd, Executable and Args mirror tmux's own StartDetached(ctx, name, cwd, executable, args) (internal/sessionlaunch/tmux.go) exactly, because that is the one concrete launch mechanism this Feature moves behind the interface today; herdr's Task 4 launch (if any) documents its own use of these same fields, or extends this type when it genuinely needs something tmux never did — not before that need is concrete.
There is deliberately no Name field: the terminal's own name is SuccessorWBSessionID run through Transport.AddressFor — the identical derivation MatchesReceiptIdentity checks a receipt against — so Transport.Launch derives it itself rather than trusting a caller-supplied name that could disagree with AddressFor's own convention.
type LookPath ¶
LookPath matches exec.LookPath's signature. Production code passes exec.LookPath (the default when nil is given to CheckTmuxBinary); tests pass a fake so no test result depends on the host's actual PATH.
type NoneTransport ¶
type NoneTransport struct{}
NoneTransport is the first-class no-op transport REQ:none-transport-is-first-class requires: every method returns a plain, honest, non-error outcome, because there never was anywhere live to deliver to. It is the transport a session resolves to when neither herdr nor tmux identity is observable (REQ:identity-capture-outside-herdr, Task 5's to wire onto this type). The zero value is ready to use.
func (NoneTransport) AddressFor ¶
func (NoneTransport) AddressFor(string) string
AddressFor implements Transport. none has no address derivable purely from a WB session ID — there is no live pane to name at all — so it returns "".
func (NoneTransport) Capabilities ¶
func (NoneTransport) Capabilities() Capabilities
Capabilities implements Transport, returning NoneCapabilities.
func (NoneTransport) Deliver ¶
Deliver implements Transport. NoneCapabilities declares every capability false, so ResolveDeliveryMode always resolves ModeRecordOnly for it regardless of delivery.Operation and delivery.Class — every requested delivery, including a caller mistake that assumes a submit or advisory path exists, resolves to a recorded outcome and no error (REQ:none-transport-is-first-class, AC:none-transport-records-only). The explicit zero-Target check below is redundant for none specifically (its Outcome is always ModeRecordOnly regardless), but is written the way every Transport implementation must, as the reference example.
func (NoneTransport) Inspect ¶
func (NoneTransport) Inspect(context.Context, Target) (Inspection, error)
Inspect implements Transport. none never started anything, so nothing is ever live and nothing ever left terminal evidence behind either.
func (NoneTransport) Kind ¶
func (NoneTransport) Kind() Kind
func (NoneTransport) Launch ¶
func (NoneTransport) Launch(context.Context, LaunchRequest) (Target, error)
Launch implements Transport. none has no terminal to start, so it returns a zero Target with no error: launching a successor onto none is a supported, expected outcome, not a failure.
func (NoneTransport) ResolvePane ¶
ResolvePane implements Transport. none never has a live pane, so it reports ErrNoUniqueTarget rather than fabricating one; every caller already treats that as record-only (REQ:no-live-owner-is-record-only).
type Operation ¶
type Operation string
Operation names one of the two terminal-addressing responsibilities that actually place text into a pane; every other session code path REQ:single-transport-interface lists (move, park, receive, successor launch) addresses a pane through Transport.ResolvePane, Transport.Launch or Transport.Inspect instead, and confirms identity through [Transport.MatchesReceiptIdentity] — none of them deliver text, so none of them is tagged here.
const ( // OperationMessage is `wb session message`'s and `wb session // send`'s/`recall`'s target-side write // (REQ:existing-successor-messaging-binding-path). It is the only // Operation ever paired with [EventSuccessorMessage]. OperationMessage Operation = "message" // OperationWake is the daemon's own-PR-outcome and advisory wake path // (REQ:prompt-restricted-to-own-pr-outcome, // REQ:advisory-for-everything-else). It is paired with // [EventOwnPROutcome] or [EventAdvisory], never [EventSuccessorMessage]. OperationWake Operation = "wake" )
type OverrideConfig ¶
type OverrideConfig struct {
Transport Kind `yaml:"transport"`
}
OverrideConfig is the `session` section of `wb.yaml` ([wbconfig.DefaultPath] by default) that REQ:explicit-transport-override reads. An equivalent command flag carries the identical Kind value directly into ResolveOverride without going through this file at all — Task 2 ships the validation both routes share, not a CLI flag itself (no production call site is rewired yet).
type PrerequisiteCheck ¶
PrerequisiteCheck reports whether kind's runtime prerequisite (a tmux binary on PATH, a resolvable herdr binary) is usable right now. It returns a descriptive error rather than a bool so ResolveOverride's refusal is actionable, per REQ:explicit-transport-override. A check that has nothing to say about kind (a herdr check asked about tmux) MUST return nil, not an unrelated error.
func CheckHerdrBinary ¶
func CheckHerdrBinary(lookup herdr.EnvLookup) PrerequisiteCheck
CheckHerdrBinary is a PrerequisiteCheck for KindHerdr: it fails closed when the herdr binary cannot be resolved via HERDR_BIN_PATH or PATH (internal/herdr.ResolveBinary, Task 1's landed adapter — this package goes through it rather than calling exec.LookPath("herdr") itself, matching Task 1's "every other task that needs herdr goes through this package" rule). It does not check herdr socket reachability: Task 10 owns herdr's remaining distinct failure modes (missing command, socket unreachable, version drift). It reports nil for every other Kind. A nil lookup defaults to herdr.OSLookupEnv.
func CheckTmuxBinary ¶
func CheckTmuxBinary(lookPath LookPath) PrerequisiteCheck
CheckTmuxBinary is a PrerequisiteCheck for KindTmux: it fails closed, naming the missing binary, when tmux cannot be resolved on PATH. It reports nil for every other Kind. A nil lookPath defaults to exec.LookPath.
func ComposeChecks ¶
func ComposeChecks(checks ...PrerequisiteCheck) PrerequisiteCheck
ComposeChecks combines several PrerequisiteCheck values into one that fails closed on the first check that fails. Production wiring (Task 4's herdr check, Task 10's distinct failure modes) combines checks this way so neither later task needs to reinvent composition. A nil entry is skipped.
type Receipt ¶
type Receipt struct {
Operation Operation
Outcome DeliveryMode
Target Target
At time.Time
}
Receipt is the honest outcome of one Transport.Deliver call. Outcome reports what actually happened, which MUST NOT exceed (DeliveryModeExceeds) what ResolveDeliveryMode computes from the transport's declared Capabilities, delivery.Operation and delivery.Class — a transport must downgrade and report ModeRecordOnly rather than claim it advised or submitted when it did not (REQ:recorded-only-receipt-state's principle, applied here to this package's own in-memory Receipt rather than to sessionmove.MessageReceipt's wire schema, which Task 4 owns separately). It MAY report a strictly less binding mode than that computed value — Transport.Deliver documents the one case every implementation shares, a zero Target.
type Target ¶
type Target struct {
Kind Kind
// ID is the transport-native pane identifier used to address a live
// send call: herdr's pane ID (e.g. "w1:p2") for `pane send-text`, or
// tmux's own pane ID (e.g. "%7") for `paste-buffer -t` — see
// internal/sessionmessage/tmux.go's Pane.ID, PasteBuffer's paneID
// argument.
ID string
// Name is tmux's session name, "wb-session-" + a WB session ID
// (internal/sessionlaunch/launch.go:482,
// internal/sessionmessage/receive.go's handoffReceipt.TmuxName). Empty
// on herdr and none, which have no equivalent deterministic name.
Name string
// PID is the resolved target's live process ID — tmux's
// list-panes `#{pane_pid}` (internal/sessionlaunch/tmux.go's PanePID,
// internal/sessionmessage/tmux.go's Pane.PID). Zero when unknown.
PID int
// Socket is the herdr socket path this Target's ID is scoped to —
// herdr's IDs are scoped to one server. Empty on tmux and none.
Socket string
}
Target names a resolved, live terminal endpoint. Every field is exactly what a real call site (internal/sessionlaunch, internal/sessionmessage) needs to address it — not an opaque bag — because the two shipped transports address panes in genuinely different, concrete ways: herdr by a socket-scoped pane ID, tmux by a session name plus a separate buffer-target pane ID and a PID it corroborates against a WB session record. A field is empty when that transport does not use it.
func (Target) IsZero ¶
IsZero reports whether t names no live endpoint at all — the honest shape of "no live owner" (REQ:no-live-owner-is-record-only) and of "resolution failed" before ErrNoUniqueTarget is even considered.
type Transport ¶
type Transport interface {
// Kind reports which shipped transport this is. It exists for logging
// and for a Capabilities().Kind cross-check, never for a caller to
// branch on — that would violate REQ:single-transport-interface.
Kind() Kind
// Capabilities reports this transport's declared, fixed capability
// matrix (REQ:transport-capability-matrix). An implementation returns a
// constant value; [ResolveDeliveryMode] is the only place that
// interprets it into a delivery decision.
Capabilities() Capabilities
// ResolvePane resolves identity to a live [Target]. It MUST return
// [ErrNoUniqueTarget] — never guess, and never fall back to a stale
// recorded target — when zero or more than one live candidate matches
// (REQ:pane-resolved-by-session-identity-match). See
// [ErrNoUniqueTarget]'s own doc for exactly which callers must treat
// that as record-only today, and which pre-existing caller does not yet.
ResolvePane(ctx context.Context, identity Identity) (Target, error)
// Launch starts a brand-new terminal for a successor session and
// returns the [Target] it resolves to (internal/sessionlaunch's
// StartDetached, moved behind this method by Task 3). It derives the
// terminal's own name itself, via
// AddressFor(request.SuccessorWBSessionID) — see [LaunchRequest] for
// why that field does not exist on the request instead.
Launch(ctx context.Context, request LaunchRequest) (Target, error)
// Inspect reports target's current liveness and, when it is not live,
// whatever terminal evidence is available — replacing tmux's separate
// PanePID/PaneFailure poll (internal/sessionlaunch's waitReady,
// waitExecSuccess, selectAttemptForStart, failedAttemptRetryable,
// validateAbandonment all poll this repeatedly).
Inspect(ctx context.Context, target Target) (Inspection, error)
// AddressFor returns the deterministic address this transport would use
// to reach wbSessionID's live terminal, in this transport's own naming
// scheme. tmux's is "wb-session-" + wbSessionID
// (internal/sessionlaunch/launch.go:482,
// internal/sessionmove/route.go:428,
// internal/sessionmove/store.go:521,
// internal/sessionpark/protocol.go:167,
// internal/sessioncourier/receiver.go:121,
// internal/sessioncourier/message.go:106,
// internal/worktrees/session_custody.go,
// internal/worktrees/session_park_custody.go — every one of those
// builds or checks the identical "wb-session-"+id string today; Task 3
// routes them all through this one method instead). [Transport.Launch]
// derives a new terminal's own name by calling this itself, rather than
// accepting a name from its caller (see [LaunchRequest]). herdr and
// none have no address derivable purely from a WB session ID — herdr
// resolves a live pane by `agent_session` match instead
// (REQ:pane-resolved-by-session-identity-match) — so they return "".
AddressFor(wbSessionID string) string
// Deliver sends delivery to target. It recomputes the delivery mode
// itself from Capabilities(), delivery.Operation and delivery.Class via
// [ResolveDeliveryMode] — it MUST NOT trust a caller-supplied mode,
// because none exists on [Delivery] to trust: the resolved mode could
// otherwise go stale between a caller's own check and this call.
// [ModeRecordOnly] MUST NOT touch a live pane at all, [ModeAdvisory]
// MUST NOT submit (REQ:advisory-mechanism-and-newline-boundary governs
// herdr's exact mechanism), and [ModeSubmit] is reachable only for
// [EventSuccessorMessage] paired with [OperationMessage] on a transport
// declaring [Capabilities.LineageSubmit] (tmux, unchanged,
// REQ:tmux-parity-preserved) or, once Deferred work lands, for
// [EventOwnPROutcome] paired with [OperationWake].
//
// The returned [Receipt] reports the mode that was actually used, which
// MUST NOT exceed ([DeliveryModeExceeds]) what [ResolveDeliveryMode]
// computed — a transport MUST NOT claim to have advised or submitted
// when its own declared capabilities (or the Operation/Class pairing)
// forbid it (REQ:none-transport-is-first-class's "a supported, expected
// outcome... instead of failing the calling command" generalizes to
// every transport through this contract, not only none). It MAY report
// a strictly less binding mode: whenever target [Target.IsZero] is
// true — there is nowhere to advise or submit into — Outcome MUST be
// [ModeRecordOnly] regardless of what [ResolveDeliveryMode] computed.
Deliver(ctx context.Context, target Target, delivery Delivery) (Receipt, error)
}
Transport is the one Go interface REQ:single-transport-interface requires: every session code path that addresses a terminal — session messaging, move/park/resume delivery's identity confirmation, successor launch, and the daemon wake's record/advisory/submit path — goes through it, so no call site ever branches on herdr-vs-tmux directly. Transport selection (automatic, Task 5; explicit override, ResolveOverride) is the only place a Kind is chosen.
Task 2 ships this interface, NoneTransport as its first shipped implementation, and internal/sessiontransport/transporttest.Suite as the shared contract test both remaining implementations must pass (REQ:two-transport-implementations, AC:two-implementations-satisfy-the-same-interface). Task 3 moves today's tmux code behind it; Task 4 wires herdr onto the internal/herdr adapter.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package transporttest is the shared contract test suite REQ:two-transport-implementations requires: Task 3's tmux transport and Task 4's herdr transport must both pass it, and neither needs a distinct suite of its own to prove it satisfies sessiontransport.Transport (AC:two-implementations-satisfy-the-same-interface).
|
Package transporttest is the shared contract test suite REQ:two-transport-implementations requires: Task 3's tmux transport and Task 4's herdr transport must both pass it, and neither needs a distinct suite of its own to prove it satisfies sessiontransport.Transport (AC:two-implementations-satisfy-the-same-interface). |