chat

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Apr 30, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package chat is the substrate layer that backs coord's Post, Ask, and Subscribe on top of EdgeSync's notify service. Send and Watch route through notify.Service; Request uses raw NATS request/reply on the ask subject family. See docs/adr/0008-chat-substrate.md for the decision record covering why chat carries two substrates rather than one.

This package is internal and unexported: callers outside github.com/danmestas/bones must not depend on it. The notify.Message type leaks across the package boundary into coord where eventFromMessage translates it into coord.ChatMessage per ADR 0003's substrate-hiding rule; no notify type appears on any public coord signature.

Index

Constants

This section is empty.

Variables

View Source
var ErrClosed = errors.New("chat: manager is closed")

ErrClosed reports that a public method was called on a Manager whose Close has returned. Parallel to internal/tasks.ErrClosed and internal/holds.ErrClosed so every substrate manager surfaces the same close-race sentinel.

Functions

This section is empty.

Types

type Config

type Config struct {
	// AgentID identifies this chat instance across the substrate. It is
	// threaded through to outgoing notify.Message as the From field so
	// receivers can attribute messages back to a sender.
	AgentID string

	// ProjectPrefix is the <proj> segment used to build notify subjects
	// (notify.<proj>.<thread>) and ask subjects (<proj>.ask.<recipient>).
	// Derived at the coord layer from coord.Config.AgentID per ADR 0008;
	// the chat package takes it as pre-derived input.
	ProjectPrefix string

	// Nats is the pre-connected NATS handle from coord. The chat manager
	// does not dial its own connection — it shares the one coord opened
	// so reconnection policy, auth, and TLS remain a single-source
	// concern in coord.Config. The handle is used directly for Ask's
	// request/reply path and handed to notify.NewService for Send/Watch.
	Nats *nats.Conn

	// FossilRepoPath is the filesystem path at which notify.Service
	// persists its message log. The chat manager opens (or creates) the
	// Fossil repo at this path during Open and closes it during Close,
	// mirroring the ownership posture that internal/holds takes for its
	// JetStream KV bucket.
	FossilRepoPath string

	// MaxSubscribers caps the number of concurrent Watch callers coord
	// will hand out. Validated here so an obviously-broken value fails
	// at Open rather than at first subscribe; runtime enforcement ships
	// with Phase 3D when the Subscribe surface lands.
	MaxSubscribers int
}

Config configures Open. Every field is required; there are no silent defaults. The operator supplies the numbers — coord.Open is the enforcement point for Config.Validate and propagates its own validated inputs into this struct.

func (Config) Validate

func (c Config) Validate() error

Validate checks every Config field against its documented bounds and returns the first violation as an error. The error message follows the shape "chat.Config: <field>: <reason>". Validate is pure; it does not panic on bad operator input per invariant 9 — panics are reserved for programmer-error invariants inside Open and the wrappers.

type Manager

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

Manager owns a notify.Service plus the raw *nats.Conn handed to it by coord. Send and Watch route through the service; Request uses the raw connection for ADR 0008's Ask substrate (request/reply on <proj>.ask.<recipient>). Close releases the service and the repo Open dialed, but leaves the NATS connection alone — coord owns the connection lifecycle, and chat is a borrower.

Every public method is safe to call concurrently. Close is idempotent via an atomic CAS on closed.

func Open

func Open(ctx context.Context, cfg Config) (*Manager, error)

Open validates cfg, opens (or creates) the Fossil repo at cfg.FossilRepoPath, and wires a notify.Service on top of the repo and the caller-provided NATS connection. Constructing a Manager does not consume a goroutine; notify's Watch spawns one per call. Callers must invoke Close to release the service and the repo.

Open does not dial NATS: the connection comes pre-wired from coord so reconnection, auth, and TLS stay a single-source concern. If repo open or notify.NewService fails, earlier steps are torn down before returning so no resources leak.

func (*Manager) Close

func (m *Manager) Close() error

Close releases resources held by the Manager. It closes the notify Service (which unsubscribes any active NATS subscriptions) and then closes the Fossil repo Open dialed. The shared *nats.Conn is NOT closed here — coord owns the connection lifecycle. Subsequent calls are no-ops; safe to call more than once. Errors from service.Close are returned first; repo.Close errors are returned only when the service closed clean.

func (*Manager) ListThreads

func (m *Manager) ListThreads(ctx context.Context) ([]ThreadSummary, error)

ListThreads returns all threads for this Manager's project, sorted by last activity (most recent first).

func (*Manager) Request

func (m *Manager) Request(
	ctx context.Context, subject string, payload []byte,
) ([]byte, error)

Request sends payload to subject via NATS request/reply and returns the reply payload. ctx bounds the wait — a deadline-less ctx on an offline recipient never returns. Phase 3C's coord.Ask builds the subject as <proj>.ask.<recipient> and hands it to this method; chat itself is subject-agnostic so the same wrapper serves any future request/reply caller.

Errors from the NATS RequestWithContext path are wrapped with the chat.Request prefix so substrate failures are distinguishable from caller-contract violations. A nil Manager or nil ctx panics; empty subject panics; an empty payload is permitted because NATS treats zero-length payloads as valid.

func (*Manager) Respond

func (m *Manager) Respond(
	subject string,
	handler func(payload []byte) ([]byte, error),
) (func() error, error)

Respond registers a NATS subscription on subject that drives handler for every incoming request. handler receives the request payload and returns either a reply payload or an error. On a nil-error return, Respond publishes the reply via msg.Respond. On a non-nil error return, no reply is published — by design: the chat substrate does not model error payloads (see ADR 0008), so handler failure is surfaced to the Ask caller as a no-responders timeout. The effect is that handler errors and "no handler registered" are indistinguishable from the ask side; callers that need richer error semantics layer them in the payload shape.

The returned closure is an idempotent unsubscribe: the first call tears down the subscription; subsequent calls are no-ops and return nil. Sync.Once-guarded so concurrent callers cannot double-close the underlying subscription.

Subject is forwarded to the NATS connection as-is; chat itself is subject-agnostic. coord.Answer supplies "<proj>.ask.<agentID>".

Returns ErrClosed if the Manager has been closed. A nil Manager or nil handler panics (programmer error); empty subject panics. Any error from nats.Conn.Subscribe is wrapped with the chat.Respond prefix.

func (*Manager) Send

func (m *Manager) Send(
	ctx context.Context, thread, body string,
) error

Send publishes body to a chat thread. thread is a caller-supplied name that Manager maps to a deterministic notify Thread UUID via SHA-256(project + ":" + name). Two Managers on the same substrate that both post to "t1" compute the same Thread UUID and therefore publish on the same NATS subject — cross-Manager and cross-restart thread identity falls out of the hash with no coordination substrate.

Send bypasses notify.Service.Send because Service.Send's resolveThread rejects unknown ThreadShorts (they must have a pre-existing fossil entry), and we want to OWN the Thread UUID, not have notify generate it. Instead: build the notify.Message via notify.NewMessage (correct ID/timestamp/shape), overwrite Thread with the deterministic UUID, commit to the local repo, and publish on the computed NATS subject.

ctx is pre-checked: a canceled context short-circuits before any repo or NATS work. Once CommitMessage is entered, it runs to completion — the upstream call takes no ctx, and write latency is sub-millisecond in normal operation.

See docs/adr/0008-chat-substrate.md "Update (2026-04-19)" for the deterministic-identity scheme and its rationale.

func (*Manager) ThreadsForAgent

func (m *Manager) ThreadsForAgent(
	ctx context.Context, agentID string, maxThreads int,
) ([]ThreadSummary, error)

ThreadsForAgent returns up to maxThreads recent threads where the given agentID has sent at least one message. Threads are sorted by last activity (most recent first). Errors reading individual threads are logged and skipped so one bad thread does not fail the whole snapshot.

func (*Manager) Watch

func (m *Manager) Watch(
	ctx context.Context, thread string,
) <-chan notify.Message

Watch returns a channel of notify.Message values for the given thread. thread is a caller-supplied name — chat hashes it internally into the same deterministic ThreadShort that Send uses, so a Watch on "t1" receives every message any Manager has Sent to "t1" on this project/substrate. The channel closes when ctx is canceled.

The notify.Message type crosses the package boundary because this is an INTERNAL package — the translation into coord.ChatMessage (ADR 0003, ADR 0008) lives in coord.

A nil Manager, nil ctx, or empty thread panics (programmer error). Use-after-close returns an already-closed channel rather than panicking so deferred consumer drain stays quiet.

func (*Manager) WatchAll

func (m *Manager) WatchAll(ctx context.Context) <-chan notify.Message

WatchAll returns a channel of notify.Message values for every thread in this Manager's project. The channel closes when ctx is canceled. This is the project-wide counterpart to Watch: coord.Subscribe routes through WatchAll when the caller passes an empty pattern (ADR 0008 documents empty = project-wide), and through Watch otherwise.

Downstream, this maps to notify.Service.Watch with an empty ThreadShort — which notify interprets as a wildcard subscribe across every thread subject under the project.

A nil Manager or nil ctx panics (programmer error). Use-after-close returns an already-closed channel, same shape as Watch.

func (*Manager) WatchPattern

func (m *Manager) WatchPattern(
	ctx context.Context, pattern string,
) <-chan notify.Message

WatchPattern returns a channel of notify.Message values for every thread whose NATS subject segment matches pattern. Unlike Watch — which hashes its thread argument into a deterministic ThreadShort — WatchPattern passes pattern through to the substrate unchanged, so callers can supply NATS subject wildcards ("*" for every thread, a literal ThreadShort for a single known stream, or an already-hashed short from a ChatMessage.Thread()).

This is the substrate half of coord.SubscribePattern's glob-Subscribe surface. The raw-pattern leak is deliberate: callers see the NATS pattern shape that ADR 0003 normally hides, the payoff being a glob-Subscribe deliverable without a new KV bucket or per-Post registry writes.

Empty pattern is asserted — use WatchAll for project-wide streams. Use-after-close returns an already-closed channel, same shape as Watch. A nil Manager or nil ctx panics (programmer error).

type ThreadSummary

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

ThreadSummary is a read-only view of a chat thread for agent context recovery. Mirrors notify.ThreadSummary with only the fields needed by coord.Prime().

func (ThreadSummary) LastActivity

func (t ThreadSummary) LastActivity() time.Time

func (ThreadSummary) LastBody

func (t ThreadSummary) LastBody() string

func (ThreadSummary) MessageCount

func (t ThreadSummary) MessageCount() int

func (ThreadSummary) ThreadShort

func (t ThreadSummary) ThreadShort() string

Jump to

Keyboard shortcuts

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