realtime

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BuildEventPublisher

type BuildEventPublisher struct {
	// contains filtered or unexported fields
}

BuildEventPublisher adapts PubSub to the builder.EventPublisher interface.

Build events split across two topics: lightweight lifecycle status (agent.build, carrying tasks_done/total for the badge) broadcasts on the AGENT topic so every member's UI updates without opening the Build page; the verbose stream (log lines, live actions, todos) publishes on the per-BUILD topic, which the Build page subscribes to only while open.

func NewBuildEventPublisher

func NewBuildEventPublisher(ps *PubSub, hub *Hub) *BuildEventPublisher

NewBuildEventPublisher creates a BuildEventPublisher.

func (*BuildEventPublisher) PublishBuildEvent

func (p *BuildEventPublisher) PublishBuildEvent(ctx context.Context, agentID, buildID uuid.UUID, status, errMsg, phase string, tasksDone, tasksTotal int32)

PublishBuildEvent publishes an agent build lifecycle event on the agent topic. tasksDone/tasksTotal drive the "Building N/M tasks" badge.

func (*BuildEventPublisher) PublishBuildLogLine

func (p *BuildEventPublisher) PublishBuildLogLine(ctx context.Context, agentID, buildID uuid.UUID, seq int64, stream, line string)

PublishBuildLogLine publishes a single build log line on the per-build topic. seq is a monotonic counter per build (across both sol and docker streams); the frontend uses it to dedupe against a REST snapshot of the persisted log.

func (*BuildEventPublisher) PublishBuildTodos added in v0.4.0

func (p *BuildEventPublisher) PublishBuildTodos(ctx context.Context, buildID uuid.UUID, seq int64, todosJSON []byte)

PublishBuildTodos publishes the agent's full todo list on the per-build topic. todosJSON is the same jsonb blob persisted to agent_builds.todos.

type Conn

type Conn struct {
	ID     string
	UserID uuid.UUID
	Email  string
	// SinceSeq is the client's replay cursor from the ?since= connect
	// param: the max Envelope.Seq it has already processed. Set once by
	// the WS accept handler before the Subscribe loop; the hub replays
	// only seq>SinceSeq per topic (0 = fresh connect, no replay).
	SinceSeq uint64
	// contains filtered or unexported fields
}

Conn wraps a WebSocket connection with user identity and a send buffer.

func NewConn

func NewConn(ws *websocket.Conn, userID uuid.UUID, email string, logger *zap.Logger) *Conn

NewConn creates a new Conn for the given WebSocket and user.

func (*Conn) Close

func (c *Conn) Close()

Close closes the send channel, causing WritePump to exit.

func (*Conn) ReadPump

func (c *Conn) ReadPump(ctx context.Context, handler MessageHandler)

ReadPump reads messages from the WebSocket and dispatches to handler. Blocks until the connection closes or ctx is cancelled.

func (*Conn) Send

func (c *Conn) Send(data []byte)

Send enqueues a message for writing. Non-blocking: drops if buffer full.

func (*Conn) SendEnvelope

func (c *Conn) SendEnvelope(env Envelope)

SendEnvelope marshals and enqueues an envelope.

func (*Conn) WritePump

func (c *Conn) WritePump(ctx context.Context)

WritePump drains the send channel and writes to the WebSocket. Also sends periodic pings for liveness detection. Blocks until the send channel is closed or ctx is cancelled.

type Envelope

type Envelope struct {
	Type      string `json:"type"`
	RequestID string `json:"requestId,omitempty"`
	TopicID   string `json:"topicId,omitempty"`
	UserID    string `json:"userId,omitempty"`
	// Seq is a hub-global monotonic sequence stamped at publish. The
	// client keeps the max seq it has processed and presents it as
	// ?since= on (re)connect; the hub replays only seq>since per topic,
	// or sends a `resync` when the gap is wider than the buffer. Opaque
	// to the client ("bigger = newer"); when realtime moves to a shared
	// bus the source changes without touching the wire contract.
	Seq            uint64          `json:"seq,omitempty"`
	ConversationID string          `json:"conversationId,omitempty"`
	Subagent       *SubagentInfo   `json:"subagent,omitempty"`
	Payload        json.RawMessage `json:"payload,omitempty"`
}

Envelope is the JSON wire format for all WebSocket messages.

UserID is the principal who "owns" the event — typically the human at the top of a run chain. Subscribers gate delivery on env.UserID == conn.UserID, so a tenant admin who happens to be a member of agent X does not see live events from runs another user started on X. Pre-existing system-level broadcasts leave it empty and fall through to the "deliver to every subscriber on the topic" behaviour. ConversationID lets the frontend route an event to the correct chat card without payload introspection. Subagent tags an envelope as a sub-run event so the chat store can render it underneath the parent run's tool-call instead of as a top-level message.

func NewEnvelope

func NewEnvelope(eventType, topicID string, payload proto.Message) Envelope

NewEnvelope creates an Envelope, marshaling the payload via protojson. The proto.Message constraint provides compile-time enforcement — passing map[string]any or dbq types is a compile error.

This constructor leaves UserID empty, which means "deliver to every subscriber on the topic" — appropriate for system-level broadcasts (agent.synced, build events). Per-user events should use NewEnvelopeForUser so the WS hub can apply the user-id gate.

func NewEnvelopeForUser added in v0.4.0

func NewEnvelopeForUser(eventType, topicID, userID, conversationID string, payload proto.Message) Envelope

NewEnvelopeForUser is NewEnvelope plus per-user gating + conversation routing. Callers that publish run events (text-delta, tool-call, etc.) use this so subscribers other than the run's owner don't receive the live stream.

func (Envelope) WithSubagent added in v0.4.0

func (e Envelope) WithSubagent(info SubagentInfo) Envelope

WithSubagent tags the envelope as a sub-run event and returns it. Chainable on the constructors above.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler routes inbound WebSocket messages. The WS upgrade handler auto-subscribes new connections to every agent the user is a member of, so the only inbound messages we accept are dynamic per-build subscriptions for the Build page (which we don't want streaming to every member by default). Anything else is logged and rejected.

func NewHandler

func NewHandler(database *db.DB, hub *Hub, pubsub *PubSub, logger *zap.Logger) *Handler

NewHandler creates a new inbound message handler.

func (*Handler) HandleMessage

func (h *Handler) HandleMessage(conn *Conn, env Envelope)

HandleMessage routes an inbound client message.

type Hub

type Hub struct {
	// contains filtered or unexported fields
}

Hub manages WebSocket connections and topic subscriptions. A single connection can be subscribed to many topics at once — typically every agent the user is a member of, wired up at WS-accept time.

func NewHub

func NewHub(logger *zap.Logger) *Hub

NewHub creates a new Hub.

func (*Hub) BroadcastToTopic

func (h *Hub) BroadcastToTopic(topicID uuid.UUID, env Envelope)

BroadcastToTopic sends an envelope to all connections subscribed to a topic.

User-id gating: if env.UserID is set, only connections whose authenticated user matches receive the event. Empty env.UserID falls through to the historical "deliver to every subscriber" behaviour (used for tenant-wide / system broadcasts: agent.synced, build events, etc).

The replay buffer stores (bytes, userID) pairs so a late subscriber gets the same gate applied on replay — without it, joining late would leak events from other users' runs that happened before the join.

func (*Hub) ClearTopicBuffer

func (h *Hub) ClearTopicBuffer(topicID uuid.UUID)

ClearTopicBuffer removes the replay buffer for a topic. Called after a terminal event (build complete/failed) since replay is no longer needed.

func (*Hub) ConnCount

func (h *Hub) ConnCount() int

ConnCount returns the number of active connections.

func (*Hub) OnTopicSubscriptionChange

func (h *Hub) OnTopicSubscriptionChange(onFirst func(uuid.UUID), onLast func(uuid.UUID))

OnTopicSubscriptionChange sets callbacks for when the first local connection subscribes to a topic and when the last unsubscribes. Used by PubSub to manage Redis subscriptions.

func (*Hub) Register

func (h *Hub) Register(conn *Conn)

Register adds a connection to the hub.

func (*Hub) SendToConnection

func (h *Hub) SendToConnection(connID string, env Envelope)

SendToConnection sends an envelope to a specific connection by ID.

func (*Hub) Subscribe

func (h *Hub) Subscribe(conn *Conn, topicID uuid.UUID)

Subscribe adds the connection to the given topic if not already subscribed. Pre-existing topic subscriptions on this connection are not affected — Subscribe is additive, so the WS accept handler can call it in a loop over the user's member agents without disturbing anything else. Replays any buffered events for the topic to the newly-subscribed connection.

func (*Hub) TopicConnCount

func (h *Hub) TopicConnCount(topicID uuid.UUID) int

TopicConnCount returns the number of connections subscribed to a topic.

func (*Hub) Unregister

func (h *Hub) Unregister(conn *Conn)

Unregister removes a connection and all its topic subscriptions, firing onLastUnsubscribe for any topic that's now empty.

func (*Hub) Unsubscribe added in v0.4.0

func (h *Hub) Unsubscribe(conn *Conn, topicID uuid.UUID)

Unsubscribe removes one topic subscription from a connection (the inverse of a single Subscribe). Used when the client leaves a dynamically-subscribed topic — e.g. the Build page unmounting its per-build subscription — while the connection's other subscriptions stay intact. Idempotent.

type MessageHandler

type MessageHandler func(conn *Conn, env Envelope)

MessageHandler processes an inbound WebSocket message from a client.

type PubSub

type PubSub struct {
	// contains filtered or unexported fields
}

PubSub delivers real-time envelopes to local WebSocket subscribers via the Hub. This is the single-instance implementation. For multi-instance fan-out (e.g. via Redis pub/sub), replace or extend this with a cross-process transport.

func NewPubSub

func NewPubSub(hub *Hub, logger *zap.Logger) *PubSub

NewPubSub creates a PubSub wired to the given Hub.

func (*PubSub) ClearTopicBuffer

func (ps *PubSub) ClearTopicBuffer(topicID uuid.UUID)

ClearTopicBuffer removes the replay buffer for a topic. Call after terminal events (run complete, build complete) so reconnecting clients don't replay stale streaming events.

func (*PubSub) Close

func (ps *PubSub) Close()

Close is a no-op for the local implementation.

func (*PubSub) Publish

func (ps *PubSub) Publish(ctx context.Context, topicID uuid.UUID, env Envelope) error

Publish delivers an envelope to all local subscribers of the topic.

type SubagentInfo added in v0.4.0

type SubagentInfo struct {
	AgentID string `json:"agentId"`
	RunID   string `json:"runId"`
	Slug    string `json:"slug,omitempty"`
}

SubagentInfo identifies a sub-run when an A2A child agent's events are mirrored to the parent's topic. Frontend chat-store reads it to attach the event to the parent run's active tool-call card.

Jump to

Keyboard shortcuts

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