Documentation
¶
Overview ¶
Package chat implements per-chat persistence: one JSON file per chat under <dir>/<chat_id>.json. The directory listing is the index. Each file is atomically rewritten on every mutation via write-temp-then-rename.
The store is the single source of truth for chat state. No sessions.json, no index.json, no event log replay. A chat's ACP session id lives in the chat file's header so a container restart can resume via session/load.
Index ¶
- func NewPurgeScheduler(ctx context.Context, store *Store, retention func() time.Duration) *archive.PurgeScheduler
- type ErrorKind
- type Router
- type Store
- func (s *Store) AppendMessage(ctx context.Context, chatID api.ChatID, msg *api.Message) error
- func (s *Store) Archive(ctx context.Context, chatID api.ChatID) error
- func (s *Store) Broadcast() api.Broadcaster
- func (s *Store) BuildHistory(ctx context.Context, chatID api.ChatID) string
- func (s *Store) ChildrenOf(ctx context.Context, parentID api.ChatID) []api.ChatID
- func (s *Store) ClearTombstone(chatID api.ChatID)
- func (s *Store) Delete(ctx context.Context, chatID api.ChatID) error
- func (s *Store) DeleteArchived(ctx context.Context, chatID api.ChatID) error
- func (s *Store) DeleteFamily(ctx context.Context, parentID api.ChatID, prepare func(api.ChatID)) (failedChildren []api.ChatID, err error)
- func (s *Store) DeletePlanDraft(_ context.Context, chatID api.ChatID) error
- func (s *Store) Dir() string
- func (s *Store) Get(ctx context.Context, chatID api.ChatID) (*api.Chat, bool)
- func (s *Store) GetPlanDraft(ctx context.Context, chatID api.ChatID) (string, error)
- func (s *Store) Header(ctx context.Context, c *api.Chat) api.ChatHeader
- func (s *Store) List(ctx context.Context) []api.ChatHeader
- func (s *Store) ListArchived(ctx context.Context) []api.ChatHeader
- func (s *Store) Load(chatID api.ChatID) (*api.Chat, error)
- func (s *Store) LoadArchived(ctx context.Context, chatID api.ChatID) (*api.Chat, error)
- func (s *Store) Lock(chatID api.ChatID) *sync.Mutex
- func (s *Store) MarkDeleted(chatID api.ChatID)
- func (s *Store) Mutate(ctx context.Context, chatID api.ChatID, ...) error
- func (s *Store) OldestCheckpoint() func(ctx context.Context, chatID api.ChatID) string
- func (s *Store) PathFor(chatID api.ChatID) (string, error)
- func (s *Store) PromoteRewind(ctx context.Context, childID api.ChatID) (api.ChatID, error)
- func (s *Store) PurgeArchived(ctx context.Context, maxAge time.Duration)
- func (s *Store) ReferencedSessionIDs(ctx context.Context) map[string]struct{}
- func (s *Store) RegisterRoutes(mux *http.ServeMux)
- func (s *Store) RestoreArchived(ctx context.Context, chatID api.ChatID) error
- func (s *Store) SetBroadcaster(b api.Broadcaster)
- func (s *Store) SetPlanDraft(ctx context.Context, chatID api.ChatID, content string) error
- func (s *Store) UpdateArchivedSummary(ctx context.Context, chatID api.ChatID, summary string) error
- func (s *Store) UpdateMessage(ctx context.Context, chatID api.ChatID, msgID string, ...) error
- type StoreError
- type StoreOption
- func WithBroadcaster(b api.Broadcaster) StoreOption
- func WithOldestCheckpointFn(fn func(ctx context.Context, chatID api.ChatID) string) StoreOption
- func WithOnArchive(fn func(chatID api.ChatID)) StoreOption
- func WithOnPurge(fn func(chatID api.ChatID)) StoreOption
- func WithPreArchive(fn func(chatID api.ChatID)) StoreOption
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewPurgeScheduler ¶
func NewPurgeScheduler(ctx context.Context, store *Store, retention func() time.Duration) *archive.PurgeScheduler
NewPurgeScheduler builds a scheduler that runs purges based on the retention value returned by `retention`. Delegates to the archive sub-package.
Types ¶
type ErrorKind ¶
type ErrorKind int
ErrorKind discriminates the class of chat store error. Using a typed enum instead of independent sentinels lets handlers dispatch via a single errors.As + switch on Kind.
const ( // ErrKindNotFound means the target chat has no file on disk. ErrKindNotFound ErrorKind = iota + 1 // ErrKindTombstoned means the target chat was recently deleted. ErrKindTombstoned // ErrKindTooLarge means the plan draft exceeded maxPlanDraftBytes. ErrKindTooLarge // ErrKindIDInUse means the target chat ID is already used by an // active (non-archived) chat. ErrKindIDInUse )
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router owns the HTTP handler surface for the chat package. It holds a *Store reference and delegates all persistence to it, separating the HTTP routing/serialisation concern from the data layer.
The Store's RegisterRoutes method delegates to Router.Register so the api.ChatStore interface contract is preserved while the HTTP concern is structurally separated.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store owns the chat directory. Each chat has its own mutex so different chats never block each other; same-chat mutations serialize.
A short-TTL tombstone set guards against the delete-during-turn race: if cmdPrompt auto-creates a chat via Mutate at the same moment the user deletes it from another tab, the delayed AppendMessage calls would otherwise re-create the chat file as a ghost. Tombstones make Mutate refuse to create for a recently-deleted id — late writes become no-ops instead of undead resurrections.
func NewStore ¶
func NewStore(dir string, opts ...StoreOption) (*Store, error)
NewStore opens (or creates) the chat directory at dir. Returns an error if the directory cannot be created or its permissions cannot be enforced — callers must fail startup rather than return a store whose every op will fail.
func (*Store) AppendMessage ¶
AppendMessage is a convenience wrapper around Mutate for the common "add one message" case. It broadcasts message_appended in addition to the usual chat_updated.
The broadcast fires only after the save succeeds — if the write fails clients never see a phantom event referencing content that was never persisted.
func (*Store) Archive ¶
Archive moves a chat from the active directory to the archive subdirectory. Delegates to the archive sub-package.
func (*Store) Broadcast ¶
func (s *Store) Broadcast() api.Broadcaster
Broadcast returns the broadcaster (may be nil).
func (*Store) BuildHistory ¶
BuildHistory returns the chat as a plain-text transcript for compression priming. Returns empty string if the chat does not exist or has no messages.
func (*Store) ChildrenOf ¶
ChildrenOf returns the IDs of chats whose ParentChatID equals parentID. Scans headers without loading messages — fast for the common case of zero children.
func (*Store) ClearTombstone ¶
ClearTombstone removes the tombstone for a restored chat.
func (*Store) Delete ¶
Delete removes the chat file and broadcasts chat_deleted. No-op if the chat does not exist. This is the only function that removes chat data. After the file is gone we mark the id tombstoned so any in-flight handler racing with the delete can't re-create it via Mutate — see markDeleted for the race the tombstone guards against.
Broadcast fires even when the chat file never existed so a stale DELETE from a second device still propagates the UI update. We skip the tombstone in that case — tombstoning a never-existed id would block a legitimate new chat using the same id for 10 minutes.
func (*Store) DeleteArchived ¶
DeleteArchived permanently removes a single archived chat. Delegates to the archive sub-package.
func (*Store) DeleteFamily ¶ added in v0.2.8
func (s *Store) DeleteFamily(ctx context.Context, parentID api.ChatID, prepare func(api.ChatID)) (failedChildren []api.ChatID, err error)
DeleteFamily removes a chat and its rewind children as one named transition (see api.ChatStore for the contract). Ordering is the point: children go FIRST, so no crash window ever leaves a child whose ParentChatID references a deleted parent — an interruption mid-family leaves whole, listed, deletable chats and a parent that still lists its survivors. The child set is re-derived here (not passed in) so the scan and the deletion happen as close together as per-chat locking allows; ChildrenOf remains advisory, so a child created concurrently with the delete can still slip through — it stays listed and deletable, which is the documented failure grade.
func (*Store) DeletePlanDraft ¶
DeletePlanDraft removes the draft file for chatID. No-op if missing.
func (*Store) GetPlanDraft ¶
GetPlanDraft returns the plan-draft markdown for chatID, or "" with no error if no draft exists. Files larger than maxPlanDraftBytes are rejected before load so an out-of-band writer (anything with access to the config volume) cannot OOM the process by planting a giant file at the draft path.
func (*Store) List ¶
func (s *Store) List(ctx context.Context) []api.ChatHeader
List returns every chat's header (no messages) sorted by UpdatedAt desc. Files that fail to parse or read are logged and skipped — one bad file must not hide the rest from the sidebar. Always returns a non-nil slice so JSON encoders emit `[]` for an empty registry rather than `null` (which the wire decoder rejects as a type error).
func (*Store) ListArchived ¶
func (s *Store) ListArchived(ctx context.Context) []api.ChatHeader
ListArchived returns headers for all archived chats, sorted by UpdatedAt desc. Delegates to the archive sub-package.
func (*Store) LoadArchived ¶
LoadArchived reads the archived chat. Delegates to the archive sub-package.
func (*Store) MarkDeleted ¶
MarkDeleted records a tombstone for the chat ID (exported for archive).
func (*Store) Mutate ¶
func (s *Store) Mutate(ctx context.Context, chatID api.ChatID, mutate func(c *api.Chat, exists bool) bool) error
Mutate is the single mutation primitive: load → apply → save → broadcast. The mutator runs under the per-chat mutex and receives the current chat (or a fresh zero-value chat if it does not exist). If mutator returns false, the mutation is aborted without side effects. If it returns true, the chat is persisted and a chat_updated event is broadcast.
To create a new chat, call Mutate with an ID that does not exist. The store pre-fills c.ID and c.CreatedAt before invoking the mutator; mutators must not overwrite these — reassigning c.ID would retarget the save to a different file under the wrong per-chat mutex, which Mutate refuses with an error so the broken caller surfaces loudly. c.CreatedAt is authoritative (derived from the original create or loaded from disk); Mutate snapshots it before the mutator runs and restores it after so a caller that accidentally overwrites it (e.g. by assigning a fresh zero value on the auto-create path) can not corrupt the sidebar sort order or the broadcast payload.
func (*Store) OldestCheckpoint ¶
OldestCheckpoint returns the checkpoint lookup function (may be nil).
func (*Store) PromoteRewind ¶ added in v0.2.8
PromoteRewind clears a rewind chat's parent linkage as one checked, per-chat-locked transition (see api.ChatStore for the contract). The validation and the clear run inside a single Mutate closure, so a concurrent mutation of the same chat cannot slip between a read and the write (the old command-level Get→Mutate pair could).
func (*Store) PurgeArchived ¶
PurgeArchived deletes archived chats older than maxAge. Delegates to the archive sub-package.
func (*Store) ReferencedSessionIDs ¶ added in v0.1.165
ReferencedSessionIDs returns the set of ACP session ids still referenced by a chat vibekit keeps — both active and archived. It backs the orphan session sweep (internal/kirosession): any on-disk KAS session NOT in this set is safe to reap. Headers carry acp_session_id, so this reuses the cached List/ListArchived reads without loading full chats.
func (*Store) RegisterRoutes ¶
RegisterRoutes wires GET /api/chats (list), GET /api/chats/{id} (one chat with paginated messages), and the archived-chat routes. Delegates to Router for structural separation of HTTP concerns.
func (*Store) RestoreArchived ¶
RestoreArchived moves a chat from the archive back to the active directory. Delegates to the archive sub-package.
func (*Store) SetBroadcaster ¶
func (s *Store) SetBroadcaster(b api.Broadcaster)
SetBroadcaster implements api.ChatStore. Prefer WithBroadcaster at construction time; this exists for interface satisfaction and test fakes.
func (*Store) SetPlanDraft ¶
SetPlanDraft writes the draft markdown for chatID atomically. Caller must have sanitised size upstream; this function enforces the byte cap and returns an error if exceeded.
Refuses to write and returns *StoreError{Kind: ErrKindNotFound} if the chat file does not exist (orphan draft pollution), or {Kind: ErrKindTombstoned} if the chat was recently deleted (delete-during-edit race).
The check + write are serialised under the per-chat mutex, same ordering rule as Mutate, so a concurrent Delete can't open the door.
func (*Store) UpdateArchivedSummary ¶
UpdateArchivedSummary rewrites an archived chat's Summary field. Delegates to the archive sub-package.
func (*Store) UpdateMessage ¶
func (s *Store) UpdateMessage(ctx context.Context, chatID api.ChatID, msgID string, mutate func(*api.Message)) error
UpdateMessage mutates an existing message by ID and broadcasts message_updated. Used by tool_call_update to reflect streaming status. No-op if the message is not found.
The broadcast fires only after the save succeeds — if the write fails clients never see a phantom event referencing content that was never persisted.
type StoreError ¶
StoreError is a typed error carrying a Kind discriminator and optional detail. Handlers can dispatch via errors.As + switch on Kind instead of N independent errors.Is chains.
func (*StoreError) Error ¶
func (e *StoreError) Error() string
func (*StoreError) Is ¶
func (e *StoreError) Is(target error) bool
Is supports errors.Is matching between *StoreError values. Two store errors are considered equal when their Kinds match; Detail is ignored. This lets tests assert on Kind without constructing an identical-Detail comparator.
type StoreOption ¶
type StoreOption func(*Store)
StoreOption configures optional dependencies on a Store at construction time. Use With* functions to create options.
func WithBroadcaster ¶
func WithBroadcaster(b api.Broadcaster) StoreOption
WithBroadcaster sets the SSE broadcaster used by the store to emit chat_created / chat_updated / chat_deleted / message_* events.
func WithOldestCheckpointFn ¶
WithOldestCheckpointFn wires the lookup used to populate ChatHeader.OldestCheckpointTag.
func WithOnArchive ¶
func WithOnArchive(fn func(chatID api.ChatID)) StoreOption
WithOnArchive registers a callback fired after a chat is archived.
func WithOnPurge ¶
func WithOnPurge(fn func(chatID api.ChatID)) StoreOption
WithOnPurge registers a callback fired after an archived chat is purged.
func WithPreArchive ¶ added in v0.1.165
func WithPreArchive(fn func(chatID api.ChatID)) StoreOption
WithPreArchive registers a callback fired BEFORE a chat's file is moved to the archive dir, so the hub can tear down the chat's in-memory state (bridge, terminals, pending perms/trust, .partial) while the record is still active. See archive.WithPreArchive.