frames

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: May 19, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package frames defines the on-the-wire frame format used by every Nexus-component WebSocket connection (aspect↔Outpost, aspect↔Nexus, Outpost↔Nexus). Per transport spec v0.1 §5.

Every frame is a JSON object with a `kind` discriminator and an opaque payload the handler interprets per-kind. Unknown kinds are forward-compat: receivers log and drop rather than error out, so new frame types roll out without hard-synchronised upgrades.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoPayload = errors.New("frames: no payload")

ErrNoPayload is returned by PayloadAs when the envelope carries no payload. Callers who legitimately expect an empty payload (e.g. Shutdown with defaults) should use errors.Is to branch on it; everyone else should treat it as a bug and fail loud.

Functions

func Encode

func Encode(env Envelope) ([]byte, error)

Encode serialises an envelope to JSON.

func IsKnown

func IsKnown(k Kind) bool

IsKnown returns true if the kind is one the current build recognises. Callers that receive an unknown kind should log and drop the frame — do not error out (forward-compat: new kinds roll out without synchronised upgrades).

func PayloadAs

func PayloadAs(env Envelope, dst any) error

PayloadAs unmarshals the envelope's payload into dst. Returns ErrNoPayload when the envelope has no payload at all — callers who legitimately expect an empty payload (Shutdown with defaults, etc.) should branch on errors.Is(err, ErrNoPayload); everyone else should treat it as a bug. Silent no-op would hide routing mistakes.

Types

type AgentSayPayload

type AgentSayPayload struct {
	AspectID string `json:"aspect_id"`
	Content  string `json:"content"`
}

AgentSayPayload — direct prompt injection bypassing chat. Used by the operator dashboard "say to agent" affordance.

type AgentStartPayload

type AgentStartPayload struct {
	AspectID string `json:"aspect_id,omitempty"`
}

AgentStartPayload — bring up an aspect (empty = "all").

type AnnounceFilePayload

type AnnounceFilePayload struct {
	From        string `json:"from"`
	Path        string `json:"path"`
	Description string `json:"description"`
}

AnnounceFilePayload surfaces a file path to chat with a brief description. Server creates a chat_messages row + shared_files row linked to it; the response (an Ack-shaped frame) carries the new chat msg_id.

type AspectActivityPayload

type AspectActivityPayload struct {
	Type      string          `json:"type"`
	AspectID  string          `json:"aspect_id"`
	EmittedAt string          `json:"emitted_at"` // RFC 3339 UTC
	Payload   json.RawMessage `json:"payload"`
}

AspectActivityPayload is Lock 5 telemetry over the wire — the out-of-process counterpart to the in-process funnel.EventSink. Aspects emit these; Nexus fans them out to dashboard activity surfaces (the activity strip, mobile "agent responding" indicator). Ephemeral — not stored, not chat posts.

Type matches funnel.EventType strings ("turn.start", "turn.end", "compact.start", "compact.end", "filter.judging", "provider.retry"). Payload is opaque JSON the dashboard layer shapes per type — keeps the frame definition stable as new event types are added.

type AspectSayPayload

type AspectSayPayload struct {
	Aspect  string `json:"aspect"`
	Content string `json:"content"`
}

AspectSayPayload posts a chat message addressed to the named aspect. Sugar over chat.send with auto-prepended "@<aspect>"; the SPA's Aspects view renders a "talk to" affordance that uses this.

type AspectSayResultPayload

type AspectSayResultPayload struct {
	MsgID int64 `json:"msg_id"`
}

AspectSayResultPayload — the new chat msg_id, so the SPA can follow up on its own message in the chat stream.

type AspectStatusPulsePayload

type AspectStatusPulsePayload struct {
	Aspect string `json:"aspect"`
	Phase  string `json:"phase"`
	Detail string `json:"detail,omitempty"`
	TS     string `json:"ts"`
}

AspectStatusPulsePayload is pushed when an aspect emits a mid-work status pulse (#118 — currently aspirational; the payload shape lands here so 5e can render UI for it once the pulse origin lights up).

type ChatDeliverPayload

type ChatDeliverPayload struct {
	ID         int    `json:"id"`
	From       string `json:"from"`
	Content    string `json:"content"`
	ReplyTo    int    `json:"reply_to,omitempty"`
	Thread     string `json:"thread,omitempty"`
	ReceivedAt string `json:"received_at"`      // RFC 3339 UTC; server-stamped at Nexus DB insert
	Reason     string `json:"reason"`           // mention | reply | thread | all
	Replay     bool   `json:"replay,omitempty"` // true iff this frame was emitted as part of a since_msg_id replay

	// ReplyCount is the number of descendants in the subtree rooted
	// at this message — recursive, not just direct children. Set by
	// chat.list (operator dashboard's main feed) so the SPA can show
	// the "N replies" expander. Live chat.deliver frames leave it
	// zero — the SPA increments locally on each incoming reply.
	ReplyCount int `json:"reply_count,omitempty"`

	// ThreadRoot is the canonical thread identity (task #226 linked-
	// list thread model). Equals the row's own id for top-level
	// messages; equals the thread-root of the reply target for
	// replies. Aspects use it to key per-thread session ids
	// (deterministic uuid_v5 of aspect_name + ":" + ThreadRoot).
	// Zero for legacy rows pre-#226 migration.
	ThreadRoot int `json:"thread_root,omitempty"`
}

ChatDeliverPayload is a message being delivered to an aspect that should see it (mentioned, reply, thread participant, etc.).

Lock 6 (operator #9206/#9213/#9218): ReceivedAt is the message's server-stamped Nexus-side ingestion time, RFC 3339 UTC. Aspects surface this to the model on replay so deliberation can decide whether a stale request is still actionable. Same field for live frames (near-zero age) and replay frames (potentially hours old).

The ID field is the chat msg_id, which doubles as the cursor for Lock 6's replay-via-DB-query path: aspects persist the highest ID they've processed and pass it as `since_msg_id` on register.

type ChatListPayload

type ChatListPayload struct {
	AfterID  int64 `json:"after_id,omitempty"`
	BeforeID int64 `json:"before_id,omitempty"`
	Limit    int   `json:"limit,omitempty"`
}

ChatListPayload is the operator-scoped chat feed read. id-based pagination: AfterID returns messages with id > AfterID; BeforeID returns id < BeforeID. Both zero = newest page (uses a default- limit's worth of newest rows).

Distinct from ChatReadPayload, which is thread-scoped and available to aspects. Operator dashboard uses this for the main "all chat" feed; the topic-scoped variant is deferred (chat_messages has no persisted topic column today — schema migration required).

type ChatListResultPayload

type ChatListResultPayload struct {
	Messages []ChatDeliverPayload `json:"messages"`
	HasMore  bool                 `json:"has_more"`
}

ChatListResultPayload — messages oldest-first, plus has_more for "load older" affordance at the page boundary.

type ChatReactionPayload

type ChatReactionPayload struct {
	From  string `json:"from"`
	MsgID int    `json:"msg_id"`
	Emoji string `json:"emoji"`
}

ChatReactionPayload toggles an emoji reaction.

type ChatReactionUpdatePayload

type ChatReactionUpdatePayload struct {
	MsgID     int           `json:"msg_id"`
	Reactor   string        `json:"reactor"`
	Emoji     string        `json:"emoji"`
	Op        string        `json:"op"` // "added" | "removed"
	Reactions []ReactionRow `json:"reactions"`
}

ChatReactionUpdatePayload is the push frame broadcast when a chat reaction toggles. Carries the FULL current reactions list for the affected message (not a delta) so the SPA can replace in-place without merge logic. Reactor + emoji + op are included for clients that want to surface "X reacted with Y" UI; clients that just want the new counts can ignore them and consume Reactions directly.

op: "added" when ToggleReaction inserted (no prior matching triple); "removed" when it deleted.

type ChatReadPayload

type ChatReadPayload struct {
	MsgID    int   `json:"msg_id,omitempty"`
	ThreadID int64 `json:"thread_id,omitempty"`
	SinceID  int64 `json:"since_id,omitempty"`
}

ChatReadPayload is a request for a specific message or thread. Response comes back as a ChatReadResultPayload.

Lock 2 pull path: aspects use this to read context they weren't pushed, without triggering a fresh deliberation cycle. SinceID caps how far back the response includes (e.g. for paginated re-reads of a long thread).

type ChatReadResultPayload

type ChatReadResultPayload struct {
	Messages []ChatDeliverPayload `json:"messages"`
}

ChatReadResultPayload is the response to a ChatRead request — the thread's messages oldest-first. Limit applied server-side to bound large threads; aspects can paginate via SinceID.

type ChatRepliesPayload

type ChatRepliesPayload struct {
	ParentID int64 `json:"parent_id"`
}

ChatRepliesPayload requests every message whose reply_to == parent_id. Dashboard renders a thread view from one message.

type ChatRepliesResultPayload

type ChatRepliesResultPayload struct {
	ParentID int64                `json:"parent_id"`
	Messages []ChatDeliverPayload `json:"messages"`
}

ChatRepliesResultPayload — direct replies only (one level deep); the dashboard recurses if needed.

type ChatSendPayload

type ChatSendPayload struct {
	From     string   `json:"from"`
	Content  string   `json:"content"`
	ReplyTo  int      `json:"reply_to,omitempty"`
	Thread   string   `json:"thread,omitempty"`
	Mentions []string `json:"mentions,omitempty"`
	Topic    string   `json:"topic,omitempty"`
}

ChatSendPayload is an aspect posting to the shared chat bus.

type CredentialFetchPayload

type CredentialFetchPayload struct {
	Kind string `json:"kind"`
	Name string `json:"name,omitempty"`
}

CredentialFetchPayload is an aspect requesting a kind-typed credential from the broker's credential store.

Kind is required (e.g. "jira", "imap", "provider"). Name is optional:

  • Name unset → broker resolves via the aspect's default for that kind (aspects.default_<kind>_credential). Returns credentials.ErrNoDefault if no default configured.
  • Name set → broker fetches that named credential, checks the aspect is on its allowed_aspects list, audits.

The fetched bundle's shape is kind-specific (see credentials package docs). Caller (the MCP) unmarshals based on Kind.

type CredentialFetchResultPayload

type CredentialFetchResultPayload struct {
	Name   string         `json:"name"`
	Kind   string         `json:"kind"`
	Bundle map[string]any `json:"bundle"`
	// ExpiresAt is reserved for future server-side TTL — v1 always
	// emits empty string (no TTL). MCPs should cache the bundle for
	// the duration of their process and re-fetch on restart.
	ExpiresAt string `json:"expires_at,omitempty"`
}

CredentialFetchResultPayload returns the decrypted bundle to the aspect. The bundle is JSON-encoded as a free-form object — callers know the shape from Kind. Never logged on the broker side.

For kind='provider' the bundle is {api_shape, base_url, key, default_model?}. For kind='jira' it's {atlassian_email, atlassian_token, atlassian_subdomain}. For kind='imap' it's {host, port, user, password, ssl}.

type DeregisterPayload

type DeregisterPayload struct {
	schemas.DeregisterRequest
}

DeregisterPayload is sent on graceful shutdown.

type DispatchErrorPayload

type DispatchErrorPayload struct {
	Aspect     string `json:"aspect,omitempty"`
	DispatchID string `json:"dispatch_id,omitempty"`
	Reason     string `json:"reason"`
	Code       string `json:"code"` // "queue_full" | "hard_ceiling" | "identity_mismatch" | ...
	Active     int    `json:"active,omitempty"`
	SoftCap    int    `json:"soft_cap,omitempty"`
	Limit      int    `json:"limit,omitempty"`
}

DispatchErrorPayload signals that dispatch couldn't happen at all — queue saturated, hard-ceiling reached, identity mismatch, etc. Distinct from DispatchResult with an error field (which means the worker DID run and failed during execution).

For hard_ceiling rejections per spec §6.3, Active/SoftCap/Limit are populated so the caller can decide whether to retry, abort, or surface upward.

type DispatchPayload

type DispatchPayload struct {
	Aspect     string         `json:"aspect"`
	Thread     string         `json:"thread,omitempty"`
	DispatchID string         `json:"dispatch_id,omitempty"`
	Payload    map[string]any `json:"payload"`
}

DispatchPayload is sent by an aspect to the dispatcher to enqueue a unit of work. The dispatcher fairness-schedules and spawns a worker loaded with the dispatching aspect's home (NEXUS.md / SOUL.md / PRIMER). Per spec §2.2 queue items carry: aspect, thread, payload, submitted_at, dispatch_id. submitted_at lives on the envelope timestamp; the rest are body fields here.

type DispatchResultPayload

type DispatchResultPayload struct {
	Aspect     string         `json:"aspect"`
	Thread     string         `json:"thread,omitempty"`
	DispatchID string         `json:"dispatch_id,omitempty"`
	Output     map[string]any `json:"output"`
	Tokens     TokenUsage     `json:"tokens"`
	Model      string         `json:"model,omitempty"`
	Error      string         `json:"error,omitempty"` // non-empty if the worker ran but failed
}

DispatchResultPayload comes back once a worker has completed its turn. Identity flows: the worker booted as the dispatching aspect, so the result is attributed to that aspect (§2.1 result attribution).

type DocEntry

type DocEntry struct {
	Path     string `json:"path"` // relative to docs root
	Size     int64  `json:"size"`
	Modified string `json:"modified"` // RFC 3339 UTC
}

DocEntry is a single doc file's metadata.

type DocsGetPayload

type DocsGetPayload struct {
	Path string `json:"path"`
}

DocsGetPayload — read a single doc. Server enforces: relative path, no `..` segments, must resolve inside the docs root, must be UTF-8 text (binary docs rejected with status=400).

type DocsGetResultPayload

type DocsGetResultPayload struct {
	Path     string `json:"path"`
	Content  string `json:"content"`
	Modified string `json:"modified"`
}

DocsGetResultPayload returns the file content as UTF-8 text.

type DocsListPayload

type DocsListPayload struct {
	Path string `json:"path,omitempty"`
}

DocsListPayload — enumerate docs under the configured root. Path filter is an optional subdir relative to root; absolute paths and `..` segments rejected server-side.

type DocsListResultPayload

type DocsListResultPayload struct {
	Docs []DocEntry `json:"docs"`
}

DocsListResultPayload is the response.

type Envelope

type Envelope struct {
	Kind      Kind            `json:"kind"`
	ID        string          `json:"id,omitempty"`
	InReplyTo string          `json:"in_reply_to,omitempty"`
	TS        time.Time       `json:"ts"`
	Payload   json.RawMessage `json:"payload,omitempty"`

	// TargetAspect names the aspect this frame is destined for, when
	// the immediate WS connection isn't the aspect's own. Used by the
	// broker → outpost path for unsolicited downstream frames (turn,
	// shutdown) so the outpost can route to the right local aspect
	// (#20). Empty when the frame's destination is the connection
	// itself (the common case for direct aspects).
	TargetAspect string `json:"target_aspect,omitempty"`
}

Envelope is the shared shape of every frame.

func Decode

func Decode(raw []byte) (Envelope, error)

Decode parses a wire byte slice into an envelope. Does NOT decode the payload — callers use PayloadAs to unmarshal into a concrete type once they've inspected Kind.

func New

func New(kind Kind, payload any) (Envelope, error)

New stamps a frame with the current time and serialises the payload. The envelope is returned ready to send via the wire.

func NewRequest

func NewRequest(kind Kind, payload any) (Envelope, error)

NewRequest constructs a frame with a freshly-generated ULID as its correlation id. Callers don't pass ids in — the package owns generation so collisions are impossible across concurrent goroutines and the id is monotonic within a millisecond window. Use the returned envelope's ID to track the pending response.

func NewResponse

func NewResponse(kind Kind, inReplyTo string, payload any) (Envelope, error)

NewResponse constructs a response frame echoing the request's id into InReplyTo.

type FileAnnouncePayload

type FileAnnouncePayload struct {
	Name        string `json:"name"`
	URL         string `json:"url"` // ws://<aspect>/file/<path> or public URL
	MimeType    string `json:"mime_type,omitempty"`
	Description string `json:"description,omitempty"`
}

FileAnnouncePayload — aspect or operator publishes a file reference. The bytes stay on the announcing aspect's filesystem (ws:// URL) or a public URL (https://, gs://, s3://); Nexus stores only the reference.

type FileAnnounceResultPayload

type FileAnnounceResultPayload struct {
	ID        int64  `json:"id"`
	CreatedAt string `json:"created_at"` // RFC 3339 UTC
}

FileAnnounceResultPayload — ack with the new files-table id.

type FileDeliverPayload

type FileDeliverPayload struct {
	RequestID string `json:"request_id"`
	Content   string `json:"content,omitempty"`  // base64-encoded bytes
	Encoding  string `json:"encoding,omitempty"` // "base64"
	MimeType  string `json:"mime_type,omitempty"`
	Error     string `json:"error,omitempty"`
}

FileDeliverPayload — aspect funnel → Nexus. Carries bytes (or an error if the file is unreadable / not found / outside the home dir).

type FileFetchPayload

type FileFetchPayload struct {
	RequestID string `json:"request_id"` // correlates with the originating file.get
	Path      string `json:"path"`       // <path> from ws://<aspect>/file/<path>
}

FileFetchPayload — Nexus → aspect funnel. Internal frame. The funnel handles this directly via its service-frame dispatch table without invoking the deliberation loop or model. Funnel resolves the path component against the aspect's local filesystem (with path traversal hardening — reject `..` segments, absolute paths, paths escaping the aspect's home).

type FileGetPayload

type FileGetPayload struct {
	ID int64 `json:"id"`
}

FileGetPayload — request a specific file. Nexus inspects the URL scheme and either returns the public URL directly (https://) or dispatches a file.fetch to the owning aspect's funnel (ws://) and forwards the file.deliver response to the requester.

type FileGetResultPayload

type FileGetResultPayload struct {
	ID       int64  `json:"id"`
	Name     string `json:"name"`
	MimeType string `json:"mime_type,omitempty"`
	URL      string `json:"url,omitempty"`      // public URL — requester fetches independently
	Content  string `json:"content,omitempty"`  // base64-encoded bytes — set for ws:// references
	Encoding string `json:"encoding,omitempty"` // "base64" when Content is set
}

FileGetResultPayload — exactly one of {URL, Content} is non-empty. URL is set for public references; Content is set for ws:// references and carries the bytes inline (base64 in v0.1; binary WS frames are the post-cutover upgrade path for large assets).

type FileListPayload

type FileListPayload struct {
	Owner string `json:"owner,omitempty"` // filter by announcing aspect-id
	Limit int    `json:"limit,omitempty"` // default 50
}

FileListPayload — list announced files.

type FileListResultPayload

type FileListResultPayload struct {
	Files []FileSummary `json:"files"`
}

FileListResultPayload is the response.

type FileResultPayload

type FileResultPayload struct {
	MsgID   int64 `json:"msg_id,omitempty"`
	ShareID int64 `json:"share_id,omitempty"`
}

FileResultPayload is the ack for AnnounceFile or ShareFile. For announces, MsgID is the chat msg_id the model can reference; for shares, ShareID is the resource id. Exactly one is non-zero.

type FileSummary

type FileSummary struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	Owner       string `json:"owner"` // announcing aspect-id
	MimeType    string `json:"mime_type,omitempty"`
	Description string `json:"description,omitempty"`
	CreatedAt   string `json:"created_at"`
}

FileSummary is the metadata view returned in list. URL deliberately omitted — it's an internal routing detail, requesters always go through Nexus via file.get.

type ForwardedRegisterPayload

type ForwardedRegisterPayload struct {
	schemas.RegisterRequest
	ViaOutpostStamp

	// SinceMsgID mirrors RegisterPayload.SinceMsgID for forwarded
	// aspects: outposts MUST propagate the field if the downstream
	// aspect set it. Lock 6 replay applies regardless of whether the
	// connection is direct or routed via an Outpost.
	SinceMsgID int64 `json:"since_msg_id,omitempty"`

	// RequestReplay mirrors RegisterPayload.RequestReplay. Outposts
	// MUST propagate. Default false per NEX-131 — replay is opt-in.
	RequestReplay bool `json:"request_replay,omitempty"`
}

ForwardedRegisterPayload is what an Outpost sends up after an aspect registers locally.

type Kind

type Kind string

Kind identifies the frame type. See transport spec §5.2 for the canonical catalogue.

const (
	// Registration — the first frame on a new connection identifies
	// the speaker. Aspects send `register`; Outposts send
	// `outpost.register`. Server acks with `register.ack` or
	// `outpost.register.ack`.
	KindRegister           Kind = "register"
	KindRegisterAck        Kind = "register.ack"
	KindDeregister         Kind = "deregister"
	KindOutpostRegister    Kind = "outpost.register"
	KindOutpostRegisterAck Kind = "outpost.register.ack"
	KindOutpostDeregister  Kind = "outpost.deregister"

	// Turn dispatch — upstream asks an aspect to run a single turn.
	KindTurn       Kind = "turn"
	KindTurnResult Kind = "turn.result"

	// Dispatch — fresh-context turn run in an interchangeable worker
	// slot loaded with the dispatching aspect's identity. Per
	// hand-dispatch v0.1 §5.1: protocol vocabulary is generic.
	// `dispatch.error` when dispatch can't happen at all (queue
	// saturated, hard ceiling, identity mismatch).
	KindDispatch       Kind = "dispatch"
	KindDispatchResult Kind = "dispatch.result"
	KindDispatchError  Kind = "dispatch.error"

	// Chat — the existing comms surface in frame form.
	KindChatSend       Kind = "chat.send"
	KindChatDeliver    Kind = "chat.deliver"
	KindChatReaction   Kind = "chat.reaction"
	KindChatRead       Kind = "chat.read"
	KindChatReadResult Kind = "chat.read.result"
	KindAnnounceFile   Kind = "announce_file"
	KindShareFile      Kind = "share_file"
	KindFileResult     Kind = "file.result"
	KindAspectActivity Kind = "aspect.activity"

	// Knowledge — aspect-to-Nexus store/query.
	KindKnowledgeStore        Kind = "knowledge.store"
	KindKnowledgeSearch       Kind = "knowledge.search"
	KindKnowledgeSearchResult Kind = "knowledge.search.result"

	// Credentials — aspect-to-Nexus credential fetch. NEX-77.
	// Aspects fetch kind-typed credentials (jira/imap/provider) from
	// the broker's credential store via WS. Authentication is JWT
	// (the conn's authenticated registeredAs); aspects can't fetch
	// credentials they're not on the allowed_aspects list for.
	// Provider creds are usually consumed via ProviderEnv resolution
	// in the funnel — this frame exists for non-provider kinds (jira/
	// imap) where the MCP needs the bundle directly, and for provider-
	// kind plaintext-fetch (mode=fetch|both) when the aspect needs the
	// raw API key for a non-proxy code path.
	KindCredentialFetch       Kind = "credential.fetch"
	KindCredentialFetchResult Kind = "credential.fetch.result"

	// Session observability — projection upward for dashboard view.
	// Local aspect JSONL is source of truth; Nexus keeps a read-only
	// mirror for rendering.
	KindSessionEntryAppended Kind = "session.entry.appended"
	KindSessionRewind        Kind = "session.rewind"
	KindSessionFork          Kind = "session.fork"

	// Lifecycle.
	KindShutdown Kind = "shutdown"

	// switch.surface — aspect requests a live surface flip
	// (funnel ↔ agora). Broker validates, updates the DB, closes
	// the WS connection. Aspect exits; supervisor restarts with
	// the new binary.
	KindSwitchSurface       Kind = "switch.surface"
	KindSwitchSurfaceResult Kind = "switch.surface.result"

	// Operator dashboard (dashboard-ws-port spec §3.2). Request/response
	// frames the SPA sends from the browser's WS connection. All carry
	// a correlation_id (the envelope's ID) and the broker echoes it on
	// the result. Authoritative consumers: the dashboard SPA today;
	// future operator-tooling clients can reuse the same surface.
	KindRosterList       Kind = "roster.list"
	KindRosterListResult Kind = "roster.list.result"
	// chat.list is operator-only: all chat messages, paginated by id.
	// Distinct from chat.read (which is thread-scoped and aspect-
	// available). Used by the dashboard's main chat feed; topics
	// view + topic-scoped reads are a follow-up part — chat_messages
	// today has no persisted topic column, so topics work needs a
	// schema migration that's out of 5c scope.
	KindChatList             Kind = "chat.list"
	KindChatListResult       Kind = "chat.list.result"
	KindChatReplies          Kind = "chat.replies"
	KindChatRepliesResult    Kind = "chat.replies.result"
	KindReactionsFetch       Kind = "chat.reactions.fetch"
	KindReactionsFetchResult Kind = "chat.reactions.fetch.result"

	// KindChatReactionUpdate is the push frame broadcast to subscribed
	// operators when a chat reaction toggles. Carries the full reactions
	// list for the affected msg so the SPA can replace in-place — same
	// per-id shape as chat.reactions.fetch.result so the existing
	// rendering path works for both load and live-update.
	KindChatReactionUpdate   Kind = "chat.reaction.update"
	KindKnowledgeList        Kind = "knowledge.list"
	KindKnowledgeListResult  Kind = "knowledge.list.result"
	KindKnowledgeStoreResult Kind = "knowledge.store.result"
	KindAspectSay            Kind = "aspect.say"
	KindAspectSayResult      Kind = "aspect.say.result"

	// Subscription frames (5d). Each "subscribe.X" enrolls the
	// operator's connection in the corresponding push stream; the
	// matching "unsubscribe.X" turns it off. Subscriptions are
	// per-connection state, not persisted — WS close drops them.
	// Idempotent: re-subscribing is a no-op.
	KindSubscribeRoster         Kind = "subscribe.roster"
	KindSubscribeChat           Kind = "subscribe.chat"
	KindSubscribeAspectStatus   Kind = "subscribe.aspect_status"
	KindSubscribeObserve        Kind = "subscribe.observe"
	KindUnsubscribeRoster       Kind = "unsubscribe.roster"
	KindUnsubscribeChat         Kind = "unsubscribe.chat"
	KindUnsubscribeAspectStatus Kind = "unsubscribe.aspect_status"
	KindUnsubscribeObserve      Kind = "unsubscribe.observe"
	KindSubscribeAck            Kind = "subscribe.ack"
	// Push frames the broker emits to subscribed operators.
	KindRosterUpdate      Kind = "roster.update"
	KindAspectStatusPulse Kind = "aspect.status_pulse"
	KindObserveFrame      Kind = "observe.frame"

	// Upstream observability frames — sent from aspect/agentfunnel TO
	// broker so remote funnels (running in a different process from the
	// broker's observability.Hub) can stream bridle events to operator
	// dashboards via the existing observe.frame fanout.
	//
	// Attribution: the broker tags incoming events with the aspect
	// identity from the wsConn's authenticated registration
	// (wsConn.registeredAs), NOT from the payload. Per keel-cli's
	// caveat at chat #236, a mismatch between payload.Aspect and
	// registeredAs is treated as advisory and the connection's
	// authenticated identity wins.
	KindObserveBegin Kind = "observe.begin"
	KindObserveEvent Kind = "observe.event"
	KindObserveEnd   Kind = "observe.end"
)

type KnowledgeHit

type KnowledgeHit struct {
	ID        int64   `json:"id"`
	FromAgent string  `json:"from_agent"`
	Topic     string  `json:"topic"`
	Content   string  `json:"content"`
	Shared    bool    `json:"shared"`
	UpdatedAt string  `json:"updated_at"`
	Score     float64 `json:"score"`
	Matched   string  `json:"matched"`
}

KnowledgeHit mirrors the knowledge store Hit shape without importing the knowledge package into frames (keeps the dependency graph flat).

type KnowledgeListPayload

type KnowledgeListPayload struct {
	Agent string `json:"agent,omitempty"`
	Limit int    `json:"limit,omitempty"`
}

KnowledgeListPayload mirrors the knowledge.Store.List shape: scope by from_agent (omit for the operator's own entries via the caller-identity convention; explicit name for peer reads).

type KnowledgeListResultPayload

type KnowledgeListResultPayload struct {
	Entries []KnowledgeHit `json:"entries"`
}

KnowledgeListResultPayload — entries newest-updated first.

type KnowledgeSearchPayload

type KnowledgeSearchPayload struct {
	Text     string   `json:"text"`
	OwnAgent bool     `json:"own_agent,omitempty"`
	Shared   bool     `json:"shared,omitempty"`
	Peers    []string `json:"peers,omitempty"`
	TopK     int      `json:"top_k,omitempty"`
	MaxRank  float64  `json:"max_rank,omitempty"`
}

KnowledgeSearchPayload is an aspect querying the knowledge store.

type KnowledgeSearchResultPayload

type KnowledgeSearchResultPayload struct {
	Hits []KnowledgeHit `json:"hits"`
}

KnowledgeSearchResultPayload is the response.

type KnowledgeStorePayload

type KnowledgeStorePayload struct {
	Topic   string `json:"topic"`
	Content string `json:"content"`
	Shared  bool   `json:"shared,omitempty"`
}

KnowledgeStorePayload is an aspect writing a knowledge entry.

type KnowledgeStoreResultPayload

type KnowledgeStoreResultPayload struct {
	ID int64 `json:"id"`
}

KnowledgeStoreResultPayload echoes the row id back to the SPA.

type NetworkMaintenancePayload

type NetworkMaintenancePayload struct {
	Enabled bool   `json:"enabled"`
	Reason  string `json:"reason,omitempty"`
}

NetworkMaintenancePayload — toggle maintenance mode (suppress non-admin frames except status reads).

type NetworkRestartPayload

type NetworkRestartPayload struct {
	Target string `json:"target,omitempty"`
}

NetworkRestartPayload — restart whole network or a specific aspect. Empty Target = restart-all. Operator/Frame role only.

type NetworkShutdownPayload

type NetworkShutdownPayload struct {
	GracePeriodS int `json:"grace_period_s,omitempty"`
}

NetworkShutdownPayload — graceful shutdown across the network.

type ObserveBeginPayload

type ObserveBeginPayload struct {
	Aspect     string `json:"aspect,omitempty"`
	TurnID     string `json:"turn_id"`
	Label      string `json:"label"`
	Model      string `json:"model,omitempty"`
	Provider   string `json:"provider,omitempty"`
	TriggerMsg int64  `json:"trigger_msg,omitempty"`
}

ObserveBeginPayload — aspect/agentfunnel forwards a Grouper BeginTurn boundary upstream so the broker's Hub can open the same turn for this aspect on the broadcast side. Aspect is advisory; the broker authoritatively uses the wsConn's registered identity per keel-cli's attribution caveat (#236).

type ObserveEndPayload

type ObserveEndPayload struct {
	Aspect string `json:"aspect,omitempty"`
}

ObserveEndPayload — closes the in-flight turn on the broker side. No body needed beyond aspect attribution (advisory).

type ObserveEventPayload

type ObserveEventPayload struct {
	Aspect    string          `json:"aspect,omitempty"`
	EventKind string          `json:"event_kind"`
	Event     json.RawMessage `json:"event"`
}

ObserveEventPayload — one bridle.Event marshaled for upstream transport. EventKind discriminates which bridle event type is encoded in Event; the broker decodes by kind and forwards to the per-aspect Grouper's OnBridleEvent. JSON-encoding bridle events directly avoids a separate wire vocabulary at the cost of being coupled to bridle's field shapes (acceptable — bridle is pinned per nexus go.mod).

type ObserveFramePayload

type ObserveFramePayload struct {
	Aspect string          `json:"aspect"`
	Frame  json.RawMessage `json:"frame"`
}

ObserveFramePayload — server push of one observability frame to a subscriber. Frame is the package-shaped value from nexus/observability.Frame, marshaled to JSON. Aspect is also surfaced at the envelope payload level so the client doesn't need to peek into Frame to route.

type OutpostDeregisterPayload

type OutpostDeregisterPayload struct {
	OutpostID string `json:"outpost_id"`
	Reason    string `json:"reason,omitempty"`
}

OutpostDeregisterPayload — graceful Outpost shutdown.

type OutpostRegisterAckPayload

type OutpostRegisterAckPayload struct {
	HeartbeatIntervalS int `json:"heartbeat_interval_s"`
}

OutpostRegisterAckPayload is the upstream acknowledgement.

type OutpostRegisterPayload

type OutpostRegisterPayload struct {
	OutpostID    string            `json:"outpost_id"`
	Host         string            `json:"host"`
	Version      string            `json:"version"`
	Capabilities []string          `json:"capabilities"`
	StartedAt    time.Time         `json:"started_at"`
	Metadata     map[string]string `json:"metadata,omitempty"`
}

OutpostRegisterPayload carries what the Nexus needs to know about a newly-connected Outpost.

type ReactionRow

type ReactionRow struct {
	Aspect string `json:"aspect"`
	Emoji  string `json:"emoji"`
}

ReactionRow is one (aspect, emoji) reaction on a message.

type ReactionsFetchPayload

type ReactionsFetchPayload struct {
	MsgIDs []int64 `json:"msg_ids"`
}

ReactionsFetchPayload requests reactions for a batch of msg_ids. Used by the chat view when rendering a page so it can show reaction counts inline.

type ReactionsFetchResultPayload

type ReactionsFetchResultPayload struct {
	Reactions map[string][]ReactionRow `json:"reactions"`
}

ReactionsFetchResultPayload — keyed by msg_id (string in JSON because JSON object keys must be strings). Empty array when no reactions exist; missing key when msg_id wasn't in the input.

type RegisterAckPayload

type RegisterAckPayload struct {
	HeartbeatIntervalS int `json:"heartbeat_interval_s"`
	StaleAfterS        int `json:"stale_after_s"`
}

RegisterAckPayload tells the client what cadence to heartbeat at (for app-level heartbeats if/when we add them; v1 relies on WS ping/pong) and when the server will consider them stale.

type RegisterPayload

type RegisterPayload struct {
	schemas.RegisterRequest

	// SinceMsgID, when non-zero, identifies the consumer's last seen
	// msg_id. The broker uses it as the lower bound for replay (see
	// RequestReplay). Sending SinceMsgID alone no longer triggers
	// replay — explicit RequestReplay opt-in is required. Aspects
	// with no persisted cursor leave this 0.
	SinceMsgID int64 `json:"since_msg_id,omitempty"`

	// RequestReplay opts the consumer into Lock 6 replay-on-connect.
	// Default false — disconnect is a clean boundary, anything sent
	// during the gap stays in chat history but does NOT auto-push on
	// reconnect (NEX-131). Set true only for genuine catch-up cases
	// (debugging, late-spawn aspects that want full backlog). Live
	// frames after Register always flow regardless of this flag.
	RequestReplay bool `json:"request_replay,omitempty"`
}

RegisterPayload is the aspect-register frame body. Mirrors the existing RegisterRequest from shared/schemas so migration from the HTTP-register path is a shape-preserving move.

type RosterAspect

type RosterAspect struct {
	Name         string   `json:"name"`
	Status       string   `json:"status"`
	LastSeen     string   `json:"last_seen,omitempty"`
	Capabilities []string `json:"capabilities,omitempty"`
	Model        string   `json:"model,omitempty"`
	Provider     string   `json:"provider,omitempty"`
	ContextMode  string   `json:"context_mode,omitempty"`
	Role         string   `json:"role,omitempty"`
}

RosterAspect is one row in roster.list.result. Subset of the internal roster + extra metadata the dashboard's Status/Agents views render.

type RosterListPayload

type RosterListPayload struct{}

RosterListPayload is the (intentionally empty) request body. Operator's dashboard pulls the current aspect roster on view-load and on subscribe.roster reconnect; the request carries no scope — operator sees everything.

type RosterListResultPayload

type RosterListResultPayload struct {
	Aspects []RosterAspect `json:"aspects"`
}

RosterListResultPayload — newest first, all aspects, with status from the in-memory Roster.

type RosterUpdatePayload

type RosterUpdatePayload struct {
	Aspect       string   `json:"aspect"`
	Status       string   `json:"status"`
	LastSeen     string   `json:"last_seen,omitempty"`
	Capabilities []string `json:"capabilities,omitempty"`
	Model        string   `json:"model,omitempty"`
	Provider     string   `json:"provider,omitempty"`
	ContextMode  string   `json:"context_mode,omitempty"`
	// Reason names the trigger ("connect" | "disconnect" |
	// "status_change") so the SPA can render a brief notification
	// without inferring from prior state.
	Reason string `json:"reason"`
}

RosterUpdatePayload is pushed when an aspect connects, disconnects, or status-changes. The dashboard's Status / Agents views replace the row with this delta. Status mirrors AspectState.Status — "live" | "stale" | "down" — and is the broker's authoritative roster state at fan-out time.

type SessionEntryAppendedPayload

type SessionEntryAppendedPayload struct {
	Aspect    string         `json:"aspect"`
	SessionID string         `json:"session_id"`
	EntryID   string         `json:"entry_id"`
	ParentID  string         `json:"parent_id,omitempty"`
	EntryKind string         `json:"entry_kind"`
	TS        time.Time      `json:"ts"`
	Payload   map[string]any `json:"payload,omitempty"`
}

SessionEntryAppendedPayload is emitted by an aspect every time it appends to its local session JSONL. Nexus stores this in a read- only projection table for dashboard rendering. NOT a source of truth — the local JSONL owns the data.

type SessionForkPayload

type SessionForkPayload struct {
	Aspect    string `json:"aspect"`
	SessionID string `json:"session_id"`
	ForkPoint string `json:"fork_point"`
	NewHeadID string `json:"new_head_id"`
}

SessionForkPayload signals that the aspect forked to a new branch.

type SessionRewindPayload

type SessionRewindPayload struct {
	Aspect     string `json:"aspect"`
	SessionID  string `json:"session_id"`
	NewHeadID  string `json:"new_head_id"`
	PreviousID string `json:"previous_id"`
}

SessionRewindPayload signals that the aspect moved its active head to an earlier entry.

type ShareFilePayload

type ShareFilePayload struct {
	From       string   `json:"from"`
	Path       string   `json:"path"`
	Recipients []string `json:"recipients"`
}

ShareFilePayload records a direct share to recipients without a chat post. Server creates a shared_files row with recipients_json populated; response carries the share_id.

type ShutdownPayload

type ShutdownPayload struct {
	Reason       string `json:"reason"`
	GracePeriodS int    `json:"grace_period_s,omitempty"`
}

ShutdownPayload is sent upstream → aspect (or Outpost → aspects, or Nexus → Outposts) to request a graceful wind-down.

type SubscribeAckPayload

type SubscribeAckPayload struct {
	Kind string `json:"kind"` // the subscribe kind that was acked
}

SubscribeAckPayload echoes the subscription kind so the SPA can confirm which channel the ack relates to. Idempotent re-subscribes also produce an ack so the SPA's RPC layer can resolve the Promise.

type SubscribeObservePayload

type SubscribeObservePayload struct {
	Aspect   string `json:"aspect"`
	SinceSeq int64  `json:"since_seq,omitempty"`
}

SubscribeObservePayload — operator subscribes to one aspect's observability stream. SinceSeq is optional: pass 0 (or omit) for the full retained tail; pass a known sequence to only get frames newer than it (useful on reconnect after a brief drop).

type SubscribePayload

type SubscribePayload struct {
	// Topics is reserved for future topic-scoped chat subscription.
	// Empty means "all" (the only behavior in v1).
	Topics []string `json:"topics,omitempty"`
}

SubscribePayload is the body of subscribe.* frames. Currently no fields are used (subscribe.chat scoping by topics is deferred — chat_messages has no persisted topic column today). Reserved shape for forward-compat: when topic-scoping lands, add Topics here without changing the kind.

type SwitchSurfacePayload added in v0.2.0

type SwitchSurfacePayload struct {
	PrimarySurface string `json:"primary_surface"`
}

SwitchSurfacePayload is sent by an aspect to request a live surface flip (funnel ↔ agora). The broker validates ownership, updates the aspects DB, and closes the WS connection so the supervisor restarts the aspect under the new binary.

type SwitchSurfaceResultPayload added in v0.2.0

type SwitchSurfaceResultPayload struct {
	Aspect          string `json:"aspect"`
	PrimarySurface  string `json:"primary_surface"`
	PreviousSurface string `json:"previous_surface,omitempty"`
}

SwitchSurfaceResultPayload is the broker's ack to a switch.surface frame. The aspect should exit after receiving this; the supervisor restarts it with the new surface binary.

type TicketCreatePayload

type TicketCreatePayload struct {
	Title       string `json:"title"`
	Description string `json:"description,omitempty"`
	Assignee    string `json:"assignee,omitempty"`
	Priority    string `json:"priority,omitempty"` // low | normal | high | urgent
	Domain      string `json:"domain,omitempty"`
	SourceMsgID int64  `json:"source_msg_id,omitempty"`
}

TicketCreatePayload — aspect or operator creates a ticket.

type TicketDetail

type TicketDetail struct {
	TicketSummary
	Description string `json:"description,omitempty"`
	SourceMsgID int64  `json:"source_msg_id,omitempty"`
	UpdatedAt   string `json:"updated_at"`
	ClosedAt    string `json:"closed_at,omitempty"`
}

TicketDetail extends TicketSummary with description + lifecycle timestamps. Returned by ticket.get; not used for list rows.

type TicketGetPayload

type TicketGetPayload struct {
	ID int64 `json:"id"`
}

TicketGetPayload — fetch one ticket with description + notes.

type TicketGetResultPayload

type TicketGetResultPayload struct {
	Ticket TicketDetail `json:"ticket"`
	Notes  []TicketNote `json:"notes"`
}

TicketGetResultPayload pairs a ticket with its notes.

type TicketListPayload

type TicketListPayload struct {
	Assignee string `json:"assignee,omitempty"`
	Status   string `json:"status,omitempty"`
	Creator  string `json:"creator,omitempty"`
	Domain   string `json:"domain,omitempty"`
	Limit    int    `json:"limit,omitempty"`
}

TicketListPayload — combinable filters; Limit caps at 200, default 50.

type TicketListResultPayload

type TicketListResultPayload struct {
	Tickets []TicketSummary `json:"tickets"`
}

TicketListResultPayload is the response to TicketListPayload.

type TicketNote

type TicketNote struct {
	ID        int64  `json:"id"`
	Author    string `json:"author"`
	Content   string `json:"content"`
	CreatedAt string `json:"created_at"`
}

TicketNote is one entry in a ticket's chronological note thread.

type TicketNoteAddPayload

type TicketNoteAddPayload struct {
	TicketID int64  `json:"ticket_id"`
	Content  string `json:"content"`
}

TicketNoteAddPayload — append a progress note. Author derives from the connection's identity, not from the payload (no spoofing).

type TicketSummary

type TicketSummary struct {
	ID        int64  `json:"id"`
	Title     string `json:"title"`
	Status    string `json:"status"`
	Priority  string `json:"priority"`
	Domain    string `json:"domain,omitempty"`
	Assignee  string `json:"assignee,omitempty"`
	Creator   string `json:"creator"`
	CreatedAt string `json:"created_at"` // RFC 3339 UTC
}

TicketSummary is the per-row shape returned by list — projection drops description to avoid response overflow at scale.

type TicketUpdatePayload

type TicketUpdatePayload struct {
	ID          int64   `json:"id"`
	Status      *string `json:"status,omitempty"`
	Assignee    *string `json:"assignee,omitempty"`
	Priority    *string `json:"priority,omitempty"`
	Title       *string `json:"title,omitempty"`
	Description *string `json:"description,omitempty"`
	Domain      *string `json:"domain,omitempty"`
}

TicketUpdatePayload — patch fields. Pointer fields distinguish "field omitted" (nil) from "field cleared to NULL" (empty string). Mirrors the broker's `!== undefined` semantics for the same case.

type TokenUsage

type TokenUsage struct {
	Input  int `json:"input"`
	Output int `json:"output"`
	Total  int `json:"total"`
}

TokenUsage mirrors provider token accounting without pulling the providers package into every frame handler.

type TurnPayload

type TurnPayload struct {
	Prompt        string `json:"prompt"`
	SystemPrompt  string `json:"system_prompt,omitempty"`
	Model         string `json:"model,omitempty"`
	ThinkingLevel string `json:"thinking_level,omitempty"`
	MaxTokens     int    `json:"max_tokens,omitempty"`
}

TurnPayload is sent upstream → aspect to trigger a single turn.

type TurnResultPayload

type TurnResultPayload struct {
	Output     string     `json:"output"`
	StopReason string     `json:"stop_reason"`
	Tokens     TokenUsage `json:"tokens"`
	EntryIDs   []string   `json:"entry_ids"`
}

TurnResultPayload is the aspect's reply.

type UnsubscribeObservePayload

type UnsubscribeObservePayload struct {
	Aspect string `json:"aspect"`
}

UnsubscribeObservePayload — operator drops one aspect from its observability subscription set.

type UsageQueryPayload

type UsageQueryPayload struct {
	Period  string `json:"period,omitempty"`   // 1h | 24h | 7d | 30d (default 7d)
	Aspect  string `json:"aspect,omitempty"`   // filter to one aspect
	GroupBy string `json:"group_by,omitempty"` // aspect | msg_id | day (default aspect)
}

UsageQueryPayload — period bucket + optional aspect filter + group_by dimension. Backed by the chat_usage table (F3.1).

type UsageQueryResultPayload

type UsageQueryResultPayload struct {
	Period string     `json:"period"`
	Rows   []UsageRow `json:"rows"`
}

UsageQueryResultPayload is the response.

type UsageRow

type UsageRow struct {
	Key          string `json:"key"`
	InputTokens  int64  `json:"input_tokens"`
	OutputTokens int64  `json:"output_tokens"`
	TotalTokens  int64  `json:"total_tokens"`
}

UsageRow is one aggregated bucket. Key shape depends on GroupBy: aspect-id, msg-id (string-rendered int), or YYYY-MM-DD.

type ViaOutpostStamp

type ViaOutpostStamp struct {
	ViaOutpost string `json:"via_outpost,omitempty"`
}

ViaOutpostStamp is attached to aspect registration frames that are forwarded upward by an Outpost. Nexus uses it to record the route. Serialised as a sibling field on the forwarded register payload.

Jump to

Keyboard shortcuts

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