hub

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

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

func TryAcquireMaintenanceLock(storeDir string) (release func(), ok bool)

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

func Join(storeDir string, sess *chat.Session, sink Sink) *Handle

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.

func (*Handle) Leave

func (h *Handle) Leave()

Leave unwinds this process's hub membership. Safe to call on a nil Handle or more than once.

type Sink

type Sink func(events.Event)

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.

Jump to

Keyboard shortcuts

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