Documentation
¶
Overview ¶
Package gateway — simple PostgreSQL-backed bookmark store for session bookmarks.
Package gateway — shared helpers for gateway implementations.
Package gateway is the shared abstraction behind every remote messaging channel — Telegram, Discord, WhatsApp, and arbitrary sidecars via the HTTP bridge. The agent runs in the TUI; gateways pipe inbound messages through to it and stream replies back, so a user can step away from their terminal and keep driving the same session from their phone.
Slack predates this package and keeps its own session map for historical reasons. New channels live here.
Package gateway — shared helpers for gateway implementations.
Index ¶
- func ChunkAtRune(s string, max int) (head, tail string)
- func NewSessionID(prefix string) string
- func TruncateMessage(msg string, maxLen int) string
- type AgentSender
- type Bookmark
- type BookmarkFn
- type BookmarkStore
- func (s *BookmarkStore) Delete(id int) error
- func (s *BookmarkStore) DeleteByLabel(sessionID, label string) error
- func (s *BookmarkStore) List() ([]Bookmark, error)
- func (s *BookmarkStore) ListBySession(sessionID string) ([]Bookmark, error)
- func (s *BookmarkStore) Save(sessionID, label string) (*Bookmark, error)
- type Channel
- type Dispatcher
- type GatewayLimitsCfg
- type HealthChecker
- type Hub
- type Inbound
- type InboundImage
- type InputHistory
- func (ih *InputHistory) Append(ctx context.Context, chatKey, text string) error
- func (ih *InputHistory) Get(ctx context.Context, chatKey string, offset int) (string, error)
- func (ih *InputHistory) GetHistory(ctx context.Context, chatKey string, limit int) ([]string, error)
- func (ih *InputHistory) Len(ctx context.Context, chatKey string) (int, error)
- type InputHistoryStore
- type MessageDelivery
- func (md *MessageDelivery) GetUndelivered(ctx context.Context, sessionID string) ([]UndeliveredMessage, error)
- func (md *MessageDelivery) MarkDelivered(ctx context.Context, id int64) error
- func (md *MessageDelivery) MarkFailed(ctx context.Context, id int64, errMsg string) error
- func (md *MessageDelivery) RecordPending(ctx context.Context, sessionID, chatKey, message string) (int64, error)
- func (md *MessageDelivery) RetryUndelivered(ctx context.Context, sessionID string) ([]UndeliveredMessage, error)
- type Notifier
- type PairingGate
- type RecentEntry
- type Reconnecter
- type Reply
- type SessionAgentProvider
- type SessionRouter
- func (r *SessionRouter) Bind(channel, chatKey, thread, sessionID string) error
- func (r *SessionRouter) Follow(channel, chatKey, target string) error
- func (r *SessionRouter) FollowTarget(channel, chatKey string) string
- func (r *SessionRouter) Recent(limit int) []RecentEntry
- func (r *SessionRouter) Resolve(channel, chatKey, thread, liveTUISession string) (id string, isFollow bool)
- func (r *SessionRouter) Touch(channel, chatKey, thread string) error
- type SessionStopper
- type UndeliveredMessage
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ChunkAtRune ¶
ChunkAtRune splits s at a rune boundary at or before max bytes, preferring the last newline within a 200-byte look-behind window so we don't cut mid-paragraph. If len(s) <= max, returns (s, "").
Unicode safety: the fallback walks backward through continuation bytes (0x80-0xBF, identified by &0xC0==0x80) to find the start of a multi-byte UTF-8 rune, ensuring both returned strings are valid UTF-8.
func NewSessionID ¶
NewSessionID mints a router-tagged session id. Channels can pass their own ids; this helper is provided for convenience.
func TruncateMessage ¶
TruncateMessage truncates msg to maxLen bytes, preferring clean breakpoints: first a newline, then a sentence boundary (". "), then a hard byte cut (respecting UTF-8 continuation bytes). Appends "…" when truncation occurs.
Types ¶
type AgentSender ¶
type AgentSender interface {
Stream(ctx context.Context, in string) (<-chan agent.StreamEvent, error)
SetSessionID(id string)
SessionID() string
EStop()
// Interrupt cancels the currently running stream for this agent.
// Safe to call from any goroutine. No-op if no stream is running.
Interrupt()
// SetQuestionFunc wires the clarify callback so the agent can ask
// the user questions mid-execution (§8.6).
SetQuestionFunc(fn agent.QuestionFunc)
// Undo removes the last exchange from session history.
Undo() (string, error)
// Retry replays the last user message with a fresh model call.
Retry(ctx context.Context) (string, error)
// Steer queues a guidance message for mid-run injection into the
// agent loop. Returns a confirmation string.
Steer(msg string) string
// Fork creates a new session branched from the current one with
// an optional name. Returns the new session ID.
Fork(name string) (string, error)
// Snapshot creates a git-based filesystem checkpoint with the
// given name. Returns the new commit hash.
Snapshot(name string) (string, error)
// Rollback rolls back to the Nth most recent checkpoint
// (0 = latest). Returns a human-readable summary.
Rollback(n int) (string, error)
// Snapshots returns a human-readable listing of saved checkpoints.
Snapshots() (string, error)
// SetGoal sets or updates the standing goal for the current session.
SetGoal(ctx context.Context, text string) error
// GetGoal returns the current goal text (empty string if none set).
GetGoal(ctx context.Context) (string, error)
// PauseGoal pauses the current goal (keeps it but stops injecting).
PauseGoal(ctx context.Context) error
// ResumeGoal resumes a paused goal so it injects again.
ResumeGoal(ctx context.Context) error
// ClearGoal removes the goal entirely.
ClearGoal(ctx context.Context) error
// Compact triggers context compaction on the current session.
// Returns the compaction result with before/after token counts and a summary.
Compact(ctx context.Context) (*agent.CompactResult, error)
// ExportHistory writes the current session's message history to a
// markdown file at the given path and returns the path.
ExportHistory(path string) (string, error)
// SetThinkingLevel sets the extended thinking budget.
SetThinkingLevel(level string)
// ThinkingLevel returns the current thinking level.
ThinkingLevel() string
// Mode returns the current plan/build mode ("plan" or "build").
// Plan mode means analysis-only; build mode allows execution.
Mode() string
// IsBusy returns true when the agent loop is actively executing Run().
// Used by harness to guard commands that would interrupt a running agent.
IsBusy() bool
}
AgentSender is the minimal slice of *agent.Agent that gateways call into. Kept tiny so the package never imports the cmd layer.
type Bookmark ¶
type Bookmark struct {
ID int `json:"id"`
SessionID string `json:"session_id"`
Label string `json:"label"`
CreatedAt time.Time `json:"created_at"`
}
Bookmark is a user-defined label pointing to a session.
type BookmarkFn ¶
BookmarkFn is the callback invoked when a user runs /bm <label> BookmarkFn is the callback invoked when a user runs /bm <label> in any gateway. The dispatcher resolves the active session for the chat, then asks this function to persist a bookmark with that label against that session. Return non-nil error to signal failure — the reply to the user surfaces it verbatim.
Defined as a field rather than a constructor argument so the caller can wire it after construction (some bookmark backends need the dispatcher to exist first).
type BookmarkStore ¶
type BookmarkStore struct {
// contains filtered or unexported fields
}
BookmarkStore persists session bookmarks in PostgreSQL.
func NewBookmarkStore ¶
func NewBookmarkStore(db *sql.DB) (*BookmarkStore, error)
NewBookmarkStore returns a BookmarkStore backed by db.
func (*BookmarkStore) Delete ¶
func (s *BookmarkStore) Delete(id int) error
Delete removes a bookmark by ID.
func (*BookmarkStore) DeleteByLabel ¶
func (s *BookmarkStore) DeleteByLabel(sessionID, label string) error
DeleteByLabel removes bookmarks matching a label and session.
func (*BookmarkStore) List ¶
func (s *BookmarkStore) List() ([]Bookmark, error)
List returns all bookmarks, newest first.
func (*BookmarkStore) ListBySession ¶
func (s *BookmarkStore) ListBySession(sessionID string) ([]Bookmark, error)
ListBySession returns bookmarks for a specific session.
type Channel ¶
Channel is one running gateway. Run blocks until ctx is cancelled. Name is used for logs and as the Inbound.Channel value.
type Dispatcher ¶
type Dispatcher struct {
// RejectBusy disables detached per-session queues for server-owned bridges.
RejectBusy bool
Agent AgentSender
Router *SessionRouter
Vision vision.Describer // optional; when set, attached images are
// captioned and inlined into the agent prompt
Logger *log.Logger
UpdateEvery time.Duration // batch reply edits; default 750ms
// MaxTextLen caps text per inbound message. 0 means use default (100000).
MaxTextLen int
// MaxImageBytes caps total image bytes per message. 0 means use default (20000000).
MaxImageBytes int
// MaxReqsPerMinute is the rate limit per sender per minute. 0 means use default (10).
MaxReqsPerMinute int
// Bookmark, when set, handles the /bm <label> slash command from
// any gateway. Nil leaves the command unwired; users see a
// helpful error rather than a silent no-op.
Bookmark BookmarkFn
// SessionTitler, when set, is called once per session when the
// first user message arrives, so the session Title is auto-set.
// Nil disables auto-titling. Called synchronously before the
// agent turn — should be fast (fire-and-forget is fine).
SessionTitler func(sessionID, firstMsg string)
// Pairing, when set, gates inbound messages from unknown senders.
// Unapproved senders receive a challenge code; their messages are
// not processed or persisted until the owner approves them.
Pairing PairingGate
// InputHistory, when set, records incoming messages to PostgreSQL
// for context-aware reply history. Recording happens only after
// the pairing gate passes — unapproved messages are never stored.
InputHistory InputHistoryStore
// MsgDelivery, when set, tracks message delivery status in Postgres.
// Messages are recorded as 'pending' before the provider call.
// On provider failure they're marked 'failed' — on the next Handle()
// call for the same session, failed messages are auto-retried first.
// On success they're marked 'delivered'. No message is ever lost.
MsgDelivery *MessageDelivery
// OnActivity, when set, is called every time a non-command user
// message is accepted for processing (after rate-limit and pairing
// gates). Cron systems wire their ActivityTracker.Record here so
// cron output delivery can defer until the user is idle.
OnActivity func()
// CostTracker, when set, powers the /usage slash command and the
// session.usage RPC endpoint. Nil disables usage tracking from
// gateways (the command returns a helpful error message).
CostTracker cost.Tracker
// contains filtered or unexported fields
}
Dispatcher is the shared "message arrives → reply goes out" engine Dispatcher is the shared "message arrives → reply goes out" engine every Channel hands its inbound messages to. It owns:
- session resolution via SessionRouter (incl. follow mode)
- the small set of slash commands a remote user needs to manage their session from a phone (/sessions, /attach, /follow, /new, /end, /help)
- per-session serialization so two phones blasting the same chat don't interleave turns inside the agent loop
- sender pairing gate: unapproved senders are challenged before their messages are processed
The Channel keeps owning its Reply transport; Dispatcher just calls PostInitial / Update / Final / Error against it.
func NewDispatcher ¶
func NewDispatcher(ag AgentSender, r *SessionRouter) *Dispatcher
NewDispatcher returns a Dispatcher with sensible defaults filled in.
func (*Dispatcher) ApplyGatewayLimits ¶
func (d *Dispatcher) ApplyGatewayLimits(cfg GatewayLimitsCfg)
ApplyGatewayLimits sets dispatch limits from a GatewayConfig. Zero values are ignored (the built-in defaults remain).
func (*Dispatcher) Handle ¶
func (d *Dispatcher) Handle(ctx context.Context, in Inbound, reply Reply)
Handle is called by a Channel for each Inbound it receives. Reply is the channel's own transport. Errors are logged and surfaced via reply.Error rather than returned, because gateway loops shouldn't die over one bad message.
func (*Dispatcher) SessionActive ¶
func (d *Dispatcher) SessionActive(in Inbound) bool
SessionActive checks only an existing binding; admission never creates state.
type GatewayLimitsCfg ¶
type GatewayLimitsCfg struct {
MaxTextLen int
MaxImageBytes int
RateLimitPerMin int
UpdateEveryMs int
}
GatewayLimitsCfg is a minimal interface for passing dispatch limits without importing config.
type HealthChecker ¶
type HealthChecker interface {
Healthy() bool
}
HealthChecker is implemented by gateways that can report their current health. A healthy gateway can send and receive messages.
type Hub ¶
type Hub struct {
Channels []Channel
Logger *log.Logger
// BackoffInitial is the initial backoff duration for channel
// restart attempts. Zero falls back to 1s.
BackoffInitial time.Duration
// BackoffMax is the maximum backoff cap for channel restart
// attempts. Zero falls back to 30s.
BackoffMax time.Duration
// contains filtered or unexported fields
}
Hub runs N channels concurrently, restarts ones that crash, and shuts them all down cleanly when ctx is cancelled.
func NewHub ¶
NewHub returns a Hub with logging defaulted to discard. Nil channels are filtered out.
func (*Hub) Run ¶
Run blocks until ctx is cancelled. Each channel runs in its own goroutine under a supervise loop with exponential backoff: a channel that returns (a panic or a non-ctx error) is restarted, capped at hubMaxBackoff between attempts. Old code logged the exit and let the goroutine end — one Discord/Telegram panic permanently killed that channel until the whole process was restarted.
type Inbound ¶
type Inbound struct {
Channel string // "telegram", "discord", "whatsapp", "bridge:<name>"
ChatKey string
Thread string
From string // display name or user id, for logging only
Text string
Images []InboundImage // attached photos; describer turns into prose
IsDirect bool // DM/private chat vs group
}
Inbound is one user-authored message arriving from any channel. ChatKey is the gateway-stable identifier for the conversation (e.g. "telegram:12345" or "bridge:whatsapp:+15551234"). Thread is optional — channels with no thread concept leave it empty.
type InboundImage ¶
InboundImage is one attached image. Mime is best-effort; describers sniff bytes if it's empty.
type InputHistory ¶
type InputHistory struct {
// contains filtered or unexported fields
}
InputHistory is a per-chat ring buffer of recent user messages stored in PostgreSQL. Each chat gets a single row holding a JSON array whose element [0] is the most recent message (latest-first ordering).
func NewInputHistory ¶
func NewInputHistory(db *sql.DB) (*InputHistory, error)
NewInputHistory returns an InputHistory backed by db. maxEntries defaults to 100. The caller owns the DB lifecycle.
func (*InputHistory) Append ¶
func (ih *InputHistory) Append(ctx context.Context, chatKey, text string) error
Append prepends text to the bounded history. Insert the row before locking it so simultaneous first appends serialize just like existing-chat updates. The transaction preserves existing data if reading or decoding fails.
func (*InputHistory) GetHistory ¶
func (ih *InputHistory) GetHistory(ctx context.Context, chatKey string, limit int) ([]string, error)
GetHistory returns up to limit most-recent messages.
type InputHistoryStore ¶
InputHistoryStore persists incoming messages for context-aware reply history. Implementations wrap a Postgres-backed ring buffer (see InputHistory in input_history.go).
type MessageDelivery ¶
type MessageDelivery struct {
// contains filtered or unexported fields
}
MessageDelivery tracks whether a user message was successfully delivered to the LLM provider or not. Failed messages are automatically retried on the next Handle() call for the same session — no message is ever lost.
func NewMessageDelivery ¶
func NewMessageDelivery(db *sql.DB) (*MessageDelivery, error)
NewMessageDelivery creates the message_delivery table and returns the tracker.
func (*MessageDelivery) GetUndelivered ¶
func (md *MessageDelivery) GetUndelivered(ctx context.Context, sessionID string) ([]UndeliveredMessage, error)
GetUndelivered returns all failed/pending messages for a session, ordered oldest-first so retries happen in the original order.
func (*MessageDelivery) MarkDelivered ¶
func (md *MessageDelivery) MarkDelivered(ctx context.Context, id int64) error
MarkDelivered updates a message to delivered.
func (*MessageDelivery) MarkFailed ¶
MarkFailed updates a message to failed with the error reason.
func (*MessageDelivery) RecordPending ¶
func (md *MessageDelivery) RecordPending(ctx context.Context, sessionID, chatKey, message string) (int64, error)
RecordPending stores a message before the provider call. Returns the row ID.
func (*MessageDelivery) RetryUndelivered ¶
func (md *MessageDelivery) RetryUndelivered(ctx context.Context, sessionID string) ([]UndeliveredMessage, error)
RetryUndelivered marks all undelivered messages for a session back to 'pending' so they get retried on the next Handle call. Returns the list of message texts, oldest first.
type Notifier ¶
Notifier is implemented by Channel bots that can send unsolicited messages to a target (channel, chat, JID) without a prior inbound message. Used by the §7.1 Layer 6 completion-push poller and other cron/alert systems to deliver notifications through already-open gateway connections.
func NotifierBots ¶
NotifierBots filters a slice of Channel to those that also implement Notifier, enabling the §7.1 Layer 6 completion-push poller and other cron/alert systems to deliver notifications through already-open gateway connections.
type PairingGate ¶
type PairingGate interface {
IsApproved(channel, senderID string) bool
IssueChallenge(channel, senderID string) (string, error)
}
PairingGate checks whether a sender is approved to interact with the agent. Implementations wrap a pairing store (e.g. security.PairingStore) and return true when the sender is on the allow-from list or pairing is disabled for the channel. False means the sender should receive a challenge code and their message should be discarded.
type RecentEntry ¶
type RecentEntry struct {
SessionID string
Channel string
ChatKey string
Thread string
Updated time.Time
// contains filtered or unexported fields
}
RecentEntry is one row in the /sessions list.
type Reconnecter ¶
Reconnecter is implemented by gateways that support explicit reconnection with backoff. Run handles automatic reconnection; Reconnect is the public hook for callers to trigger a reconnect externally (e.g. from a health monitor).
type Reply ¶
type Reply interface {
PostInitial(ctx context.Context, in Inbound, text string) (handle string, err error)
Update(ctx context.Context, handle, text string) error
Final(ctx context.Context, handle, text string) error
Error(ctx context.Context, handle string, err error) error
// StartTyping begins the native typing indicator if the channel supports it.
// Returns a stop function that clears the indicator. Channels without
// native typing support return a no-op stop function.
StartTyping() (stop func())
}
Reply is the surface a Channel exposes to the dispatcher to render an agent reply. PostInitial returns an opaque handle the channel uses to route Update / Final / Error back to the right message.
Channels that can't edit messages in place (SMS, email) MUST still implement Update by no-op'ing — Final is what users actually see.
type SessionAgentProvider ¶
type SessionAgentProvider interface {
AgentForSession(context.Context, string) (AgentSender, error)
}
SessionAgentProvider optionally binds operations to an immutable session. Multi-session API adapters implement this; existing single-agent gateways retain SetSessionID behavior.
type SessionRouter ¶
type SessionRouter struct {
// contains filtered or unexported fields
}
SessionRouter maps a (channel, chatKey, thread) tuple onto an overkill session id, persists the binding to disk so restarts don't lose in-flight conversations, and tracks a per-chat "follow" pointer the dispatcher consults each turn.
Follow mode is the cross-channel continuity primitive: a phone chat can call /follow tui to mirror whatever session id the TUI is currently using, so subsequent user messages route there even after the TUI swaps sessions.
func NewSessionRouter ¶
func NewSessionRouter(path string) (*SessionRouter, error)
NewSessionRouter loads the persisted file. Empty path disables persistence (used by tests). Missing file is not an error.
func (*SessionRouter) Bind ¶
func (r *SessionRouter) Bind(channel, chatKey, thread, sessionID string) error
Bind locks a chat to a specific session id and persists the binding. Touch is called on every turn to bump Updated for /sessions ordering.
func (*SessionRouter) Follow ¶
func (r *SessionRouter) Follow(channel, chatKey, target string) error
Follow puts the chat into follow mode. target = "tui" mirrors the live TUI session id; any other non-empty value pins to that session. Empty target clears follow mode. The channel prefix on the key prevents different channels (WhatsApp vs Discord) sharing the same chatKey from colliding on follow state.
func (*SessionRouter) FollowTarget ¶
func (r *SessionRouter) FollowTarget(channel, chatKey string) string
FollowTarget returns the current follow target for the chat ("", "tui", or a session id).
func (*SessionRouter) Recent ¶
func (r *SessionRouter) Recent(limit int) []RecentEntry
Recent returns up to limit bindings sorted newest-first. Used by /sessions to show "you were just here, want to resume?".
func (*SessionRouter) Resolve ¶
func (r *SessionRouter) Resolve(channel, chatKey, thread, liveTUISession string) (id string, isFollow bool)
Resolve returns the session id this chat should drive this turn. liveTUISession is the agent's current session id at call time; when a chat is in follow=tui mode we shadow it.
func (*SessionRouter) Touch ¶
func (r *SessionRouter) Touch(channel, chatKey, thread string) error
Touch bumps Updated so /sessions can sort by recency. No-op if unbound.
type SessionStopper ¶
SessionStopper lets adapters report cancellation that cannot be guaranteed. The original void AgentSender methods remain compatible with legacy agents.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package bridge is a one-size HTTP gateway any sidecar (Baileys for WhatsApp, discord.js, an SMS relay) can plug into.
|
Package bridge is a one-size HTTP gateway any sidecar (Baileys for WhatsApp, discord.js, an SMS relay) can plug into. |
|
Package discord — Discord gateway via discordgo (Batch H).
|
Package discord — Discord gateway via discordgo (Batch H). |
|
Package matrix implements a Matrix messaging gateway via raw HTTP against the Matrix Client-Server API (no heavy SDK).
|
Package matrix implements a Matrix messaging gateway via raw HTTP against the Matrix Client-Server API (no heavy SDK). |
|
Package mattermost implements a Mattermost messaging gateway via the Mattermost WebSocket API (wss://server/api/v4/websocket) for real-time events and the REST API for posting messages.
|
Package mattermost implements a Mattermost messaging gateway via the Mattermost WebSocket API (wss://server/api/v4/websocket) for real-time events and the REST API for posting messages. |
|
Package signal implements a Signal messaging gateway via signal-cli's REST API mode.
|
Package signal implements a Signal messaging gateway via signal-cli's REST API mode. |
|
Package slack — Slack gateway via Socket Mode.
|
Package slack — Slack gateway via Socket Mode. |
|
Package telegram is a minimal Bot API client used by the Telegram gateway.
|
Package telegram is a minimal Bot API client used by the Telegram gateway. |
|
whatsapp
|
|
|
cloud
Package cloud — WhatsApp Business Cloud API gateway (Batch L, official-path backend).
|
Package cloud — WhatsApp Business Cloud API gateway (Batch L, official-path backend). |
|
whatsmeow
Package whatsmeow — WhatsApp gateway via go.mau.fi/whatsmeow (Batch L, unofficial-path backend).
|
Package whatsmeow — WhatsApp gateway via go.mau.fi/whatsmeow (Batch L, unofficial-path backend). |