chat

package
v0.2.49 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: GPL-2.0, GPL-3.0 Imports: 23 Imported by: 0

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

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.

func NewRouter

func NewRouter(s *Store) *Router

NewRouter creates a Router backed by the given Store.

func (*Router) Register

func (rt *Router) Register(mux *http.ServeMux)

Register wires GET /api/chats (list), GET /api/chats/{id} (one chat with paginated messages), and the archived-chat routes.

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

func (s *Store) AppendMessage(ctx context.Context, chatID api.ChatID, msg *api.Message) error

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

func (s *Store) Archive(ctx context.Context, chatID api.ChatID) error

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

func (s *Store) BuildHistory(ctx context.Context, chatID api.ChatID) string

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

func (s *Store) ChildrenOf(ctx context.Context, parentID api.ChatID) []api.ChatID

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

func (s *Store) ClearTombstone(chatID api.ChatID)

ClearTombstone removes the tombstone for a restored chat.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, chatID api.ChatID) error

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

func (s *Store) DeleteArchived(ctx context.Context, chatID api.ChatID) error

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

func (s *Store) DeletePlanDraft(_ context.Context, chatID api.ChatID) error

DeletePlanDraft removes the draft file for chatID. No-op if missing.

func (*Store) Dir

func (s *Store) Dir() string

Dir returns the store's base directory.

func (*Store) Get

func (s *Store) Get(ctx context.Context, chatID api.ChatID) (*api.Chat, bool)

Get returns the full chat at chatID, or false if it does not exist.

func (*Store) GetPlanDraft

func (s *Store) GetPlanDraft(ctx context.Context, chatID api.ChatID) (string, error)

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) Header

func (s *Store) Header(ctx context.Context, c *api.Chat) api.ChatHeader

Header builds a ChatHeader from a Chat (exported for archive).

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) Load

func (s *Store) Load(chatID api.ChatID) (*api.Chat, error)

Load reads a chat from the active directory (exported for archive).

func (*Store) LoadArchived

func (s *Store) LoadArchived(ctx context.Context, chatID api.ChatID) (*api.Chat, error)

LoadArchived reads the archived chat. Delegates to the archive sub-package.

func (*Store) Lock

func (s *Store) Lock(chatID api.ChatID) *sync.Mutex

Lock returns the per-chat mutex for the archive package.

func (*Store) MarkDeleted

func (s *Store) MarkDeleted(chatID api.ChatID)

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

func (s *Store) OldestCheckpoint() func(ctx context.Context, chatID api.ChatID) string

OldestCheckpoint returns the checkpoint lookup function (may be nil).

func (*Store) PathFor

func (s *Store) PathFor(chatID api.ChatID) (string, error)

PathFor returns the path to the active chat file (exported for archive).

func (*Store) PromoteRewind added in v0.2.8

func (s *Store) PromoteRewind(ctx context.Context, childID api.ChatID) (api.ChatID, error)

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

func (s *Store) PurgeArchived(ctx context.Context, maxAge time.Duration)

PurgeArchived deletes archived chats older than maxAge. Delegates to the archive sub-package.

func (*Store) ReferencedSessionIDs added in v0.1.165

func (s *Store) ReferencedSessionIDs(ctx context.Context) map[string]struct{}

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

func (s *Store) RegisterRoutes(mux *http.ServeMux)

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

func (s *Store) RestoreArchived(ctx context.Context, chatID api.ChatID) error

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

func (s *Store) SetPlanDraft(ctx context.Context, chatID api.ChatID, content string) error

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

func (s *Store) UpdateArchivedSummary(ctx context.Context, chatID api.ChatID, summary string) error

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

type StoreError struct {
	Detail string
	Kind   ErrorKind
}

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

func WithOldestCheckpointFn(fn func(ctx context.Context, chatID api.ChatID) string) StoreOption

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.

Directories

Path Synopsis
Package archive implements the chat archive lifecycle: move to archive, list, restore, update summary, delete, and age-based purge.
Package archive implements the chat archive lifecycle: move to archive, list, restore, update summary, delete, and age-based purge.

Jump to

Keyboard shortcuts

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