Documentation
¶
Overview ¶
Package hub lets several `mivia` processes working the same session - a terminal TUI and, e.g., mivia-agent-desktop's spawned `mivia chat --json` process - see each other's live turns. Exactly one process per workspace (identified by the directory its context store lives in) becomes the hub; every later process for that workspace is a client. Every participant republishes its own chat.Session.EventBus stream to the hub, which rebroadcasts it to every other connected participant. There is no central/remote server: the hub is just whichever local process got there first, and ownership migrates to another process if it exits.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func TryAcquireMaintenanceLock ¶
TryAcquireMaintenanceLock attempts the same non-blocking hub.lock a membershipLoop owner takes, so a destructive maintenance operation (store compaction, a full reset) can refuse to run while any interactive process - TUI, REPL, or a desktop sidecar - is joined to this workspace's hub.
It reuses tryAcquireLock rather than a second flock path, so maintenance and ordinary hub ownership can never both believe they hold storeDir exclusively.
This is a narrower guarantee than "no other process has the store file open": a one-shot CLI invocation (another `mivia sessions gc`, a bare `mivia chat` single turn) never calls Join and so never holds hub.lock. Deliberately left open rather than closed with a second, general-purpose reader-writer lock: SQLite's own busy-fails-clean behavior (see internal/storage/compact_contention_test.go) already bounds the residual risk to a transient, retryable error, never data loss or a torn file - a locking protocol on every store-opening command would cost more than that risk warrants. Callers must not treat a successful acquisition as proof of total exclusivity, only of no live interactive session.
On success, call the returned release before the caller's own process exits; the lock is also released automatically if the process dies without calling it, so a crash mid-maintenance never wedges the workspace for future hub owners.
Types ¶
type Handle ¶
type Handle struct {
// contains filtered or unexported fields
}
Handle is a live hub membership. Leave unwinds it (releases the election lock if this process owned it, or closes the client connection) and stops the background retry loop. Safe to call once; Join's caller should defer it.
func Join ¶
Join makes sess a member of storeDir's hub - the directory its context store lives in (so hub.lock/hub.sock sit right beside context.db regardless of managed-worktree or config-path routing). This process becomes the hub owner if none exists yet, or a client of the existing one otherwise, and keeps retrying (electing a new owner if the current one disappears) for the life of the returned Handle. sink renders events received FROM other processes; nil is valid.
Join returns immediately; membership runs in a background goroutine.
type Sink ¶
Sink renders an event this process did not itself originate (received from the hub) onto whatever live surface this process presents - stdout NDJSON for line-mode, the TUI's renderer for the TUI. nil is valid: a surface with nothing useful to do with it yet just drops it.
type WireCompaction ¶
type WireCompaction struct {
Trigger string `json:"trigger"`
BeforeTokens int `json:"before_tokens"`
AfterTokens int `json:"after_tokens"`
ElidedMessages int `json:"elided_messages"`
ElidedBytes int `json:"elided_bytes"`
SourceRange contextstate.SourceRange `json:"source_range"`
SummaryVersion uint32 `json:"summary_version"`
// Summarized must cross the wire. It defaults to false, so omitting it
// does not lose information - it asserts "structural only, no summary"
// for every relayed compaction, including summarized ones, and a second
// surface renders the opposite of what happened.
Summarized bool `json:"summarized"`
// Reason mirrors events.CompactionEvent.Reason: the classified,
// content-free explanation for Summarized=false, so a second live
// surface (the relaying process) can render the same real cause the
// originating process saw instead of a generic fallback.
Reason string `json:"reason,omitempty"`
}
WireCompaction is the cross-process projection of events.CompactionEvent, including SourceRange so the reconstructed event stays valid (a zero SourceRange fails the payload's own Validate, and a reconstructed event that cannot pass its own validation is a latent trap for later consumers).
type WireEvent ¶
type WireEvent struct {
Kind string `json:"kind"`
Timestamp time.Time `json:"timestamp"`
SessionID string `json:"session_id"`
TurnID string `json:"turn_id"`
ToolCallID string `json:"tool_call_id,omitempty"`
Name string `json:"name,omitempty"`
// Detail carries KindTurnStart's user-submitted text (events.Event.Detail
// - see relayedKinds' doc comment); other relayed kinds don't use it.
Detail string `json:"detail,omitempty"`
Content string `json:"content,omitempty"`
Input string `json:"input,omitempty"`
Output string `json:"output,omitempty"`
ErrorText string `json:"error,omitempty"`
AgentTask string `json:"agent_task,omitempty"`
AgentName string `json:"agent_name,omitempty"`
AgentDepth int `json:"agent_depth,omitempty"`
// Compaction carries the typed payload for KindCompaction only. Nested and
// pointer so every other kind's wire form stays byte-identical, and
// content-free by construction so it is safe to commit to the wire
// contract (INV-AG-32).
Compaction *WireCompaction `json:"compaction,omitempty"`
}
WireEvent is the hub socket's newline-delimited JSON framing: a bounded, explicit projection of events.Event, not the raw struct - that carries an `error` field encoding/json cannot round-trip, and every internal Kind, including process-local ones (UI resize, config change) this package never wants to commit to a cross-process wire contract.