model

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: May 25, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AudioInput added in v0.4.0

type AudioInput struct {
	// Data is the full audio body in its original encoding.
	Data []byte
	// MimeType describes the encoding (e.g. "audio/ogg" for Telegram voice messages).
	MimeType string
	// Duration is the announced length of the recording, used for logging
	// and to short-circuit recognizers with hard duration limits.
	Duration time.Duration
}

AudioInput carries the raw audio payload for a single recognition call.

type Broker

type Broker interface {
	// Subscribe registers a new subscriber for the given deviceID. Channels for
	// different subscribers of the same device are independent. The returned
	// unsubscribe function is idempotent and safe to call multiple times.
	Subscribe(deviceID string) (<-chan Event, func())
	// IsActive reports whether there is at least one subscriber for the given device.
	IsActive(deviceID string) bool
	// Notify fans the event out to all subscribers. Slow subscribers are dropped.
	Notify(ev Event)
}

Broker fans out published events to active SSE subscribers within a single process. No database access: the broker receives an already-saved event (after the use-case commit) and fans it out over local channels.

Contract: Subscribe creates a subscription for a specific device. Returns an event channel and an unsubscribe function. Notify does not block the caller: if a subscriber's channel is full, the broker drops and closes it — the client will reconnect and receive missed events via events.SinceSeq using Last-Event-ID.

type Channel

type Channel interface {
	// ID returns the unique channel identifier.
	ID() string

	// Watch starts monitoring the channel and calls handler for each new message.
	// Blocks until the context is cancelled.
	Watch(ctx context.Context, handler func(context.Context, Message) error) error

	// FetchHistory fetches message history to build context.
	FetchHistory(ctx context.Context, source Source, limit int) ([]Message, error)
}

Channel watches a message source and delivers new messages to a handler.

type ChatReply

type ChatReply struct {
	// Text is the agent's reply text.
	Text string
	// TasksTouched are the UUIDs of tasks touched by agent tools during processing
	// (in order of first access; duplicates excluded).
	TasksTouched []string
	// ProjectsTouched are the UUIDs of projects touched by agent tools during processing
	// (in order of first access; duplicates excluded).
	ProjectsTouched []string
}

ChatReply is the ChatService response to a message.

type ChatService

type ChatService interface {
	// HandleMessage passes the message to the agent and returns the reply.
	HandleMessage(ctx context.Context, msg Message) (ChatReply, error)
}

ChatService is the use-case layer for processing incoming messages via the agent.

type Classification

type Classification int

Classification is the result of message classification.

const (
	// ClassSkip — the message does not contain a promise.
	ClassSkip Classification = iota
	// ClassPromise — the message contains a user promise.
	ClassPromise
)

func (Classification) String

func (c Classification) String() string

String returns the string representation of a classification.

type Classifier

type Classifier interface {
	// Classify returns the classification of a message.
	Classify(ctx context.Context, msg Message) (Classification, error)
}

Classifier classifies a message as Skip or Promise.

type CreateProjectRequest

type CreateProjectRequest struct {
	// Name is the project name (required).
	Name string
	// Slug is the project slug; if "", it is auto-generated from Name.
	Slug string
	// Description is the project description (optional).
	Description string
	// Aliases is the initial set of keyword aliases (optional; validated in the use-case layer).
	Aliases []string
}

CreateProjectRequest holds parameters for creating a project via ProjectService.

type CreateTaskRequest

type CreateTaskRequest struct {
	// ProjectID is the project UUID; "" means Inbox.
	ProjectID string
	// Summary is the short task description.
	Summary string
	// Details is the task context or details.
	Details string
	// Topic is the thematic group (optional).
	Topic string
	// Deadline is the due date (nil if not set).
	Deadline *time.Time
	// Source is the channel that initiated task creation.
	Source Source
}

CreateTaskRequest holds parameters for creating a single task via TaskService.

type CreateTasksRequest

type CreateTasksRequest struct {
	// ProjectID is the project UUID for all tasks; "" means Inbox.
	ProjectID string
	// Tasks is the list of tasks to create.
	Tasks []CreateTaskRequest
}

CreateTasksRequest holds parameters for batch task creation via TaskService.

type Cursor

type Cursor struct {
	// MessageID is the identifier of the last processed message.
	MessageID string
	// FolderID is the folder identifier (for IMAP: UIDVALIDITY).
	FolderID string
	// UpdatedAt is the time the cursor was last updated.
	UpdatedAt time.Time
}

Cursor describes the read position in a channel, enabling resume from the same point.

type Device

type Device struct {
	// ID is the device UUID.
	ID string
	// Name is the human-readable device name (e.g. "iPhone 17 Pro").
	Name string
	// Platform is the device platform: ios|android|macos|windows|linux.
	Platform string
	// TokenHash is the hex-encoded SHA256 of the bearer token.
	TokenHash string
	// APNSToken is the APNs push token (nil if not registered).
	APNSToken *string
	// FCMToken is the FCM push token (nil if not registered).
	FCMToken *string
	// CreatedAt is the time the device was registered.
	CreatedAt time.Time
	// LastSeenAt is the time of the last successful request (nil if no requests yet).
	LastSeenAt *time.Time
	// RevokedAt is the time the device was revoked (nil if active).
	RevokedAt *time.Time
}

Device describes a client device subscribed to instance events. TokenHash is the SHA256 hash of the bearer token (hex); the token itself is not stored.

type DeviceStore

type DeviceStore interface {
	// Create registers a new device within the given transaction.
	// Populates CreatedAt; ID, Name, Platform, and TokenHash must already be set.
	Create(ctx context.Context, tx *sql.Tx, d *Device) error
	// FindByTokenHash looks up an active (non-revoked) device by its SHA256 bearer token hash.
	// Returns nil, nil if the device is not found or is revoked.
	FindByTokenHash(ctx context.Context, hash string) (*Device, error)
	// UpdateLastSeen updates the time of the device's last successful request.
	UpdateLastSeen(ctx context.Context, id string, at time.Time) error
	// Revoke marks a device as revoked. Repeated calls for an already-revoked device
	// do not change RevokedAt.
	Revoke(ctx context.Context, id string) error
	// List returns all devices (including revoked), sorted by CreatedAt ascending.
	List(ctx context.Context) ([]Device, error)
	// ListActiveIDs returns the IDs of all non-revoked devices.
	ListActiveIDs(ctx context.Context) ([]string, error)
	// UpdatePushTokens updates the APNs/FCM push tokens for a device.
	// A nil value clears the corresponding token.
	UpdatePushTokens(ctx context.Context, id string, apns, fcm *string) error
	// Get returns a device by ID (including revoked).
	// Returns nil, nil if the device is not found.
	Get(ctx context.Context, id string) (*Device, error)
	// ListInactive returns active (non-revoked) devices whose last activity
	// (COALESCE(last_seen_at, created_at)) is strictly older than cutoff.
	// Used by the retention runner to auto-revoke stale devices.
	ListInactive(ctx context.Context, cutoff time.Time) ([]Device, error)
	// DeleteRevokedOlderThan deletes devices that were revoked strictly before cutoff.
	// Returns the number of deleted rows.
	DeleteRevokedOlderThan(ctx context.Context, cutoff time.Time) (int64, error)
}

DeviceStore stores client devices subscribed to instance events.

Create accepts an external transaction because device registration may occur atomically with other records (events, push_queue) in future phases. Other write methods operate through the internal *sql.DB — they update a single row with no relation to other tables.

type Event

type Event struct {
	// Seq is the monotonic sequence number assigned by EventStore.Insert.
	Seq int64
	// Kind is the event type (see EventKind constants).
	Kind EventKind
	// EntityID is the entity identifier (task.ID, project.ID, etc.).
	EntityID string
	// Payload is a JSON snapshot of the entity at the time of the event.
	Payload json.RawMessage
	// CreatedAt is the time the event was published.
	CreatedAt time.Time
}

Event describes a single event published by the domain model. Populated by the use case within a transaction and saved to the EventStore.

type EventKind

type EventKind string

EventKind classifies the type of event raised in the domain model.

const (
	// EventTaskCreated — a task was created.
	EventTaskCreated EventKind = "task_created"
	// EventTaskUpdated — task fields changed (details, deadline, etc.).
	EventTaskUpdated EventKind = "task_updated"
	// EventTaskCompleted — a task was marked done.
	EventTaskCompleted EventKind = "task_completed"
	// EventTaskReopened — a task was returned to open status.
	EventTaskReopened EventKind = "task_reopened"
	// EventTaskMoved — a task was moved to another project.
	EventTaskMoved EventKind = "task_moved"
	// EventTaskDeleted — a task was physically deleted by the retention sweep.
	// Emitted only by the daily cleanup of done/cancelled tasks older than
	// closed_retention; no other operation deletes tasks.
	EventTaskDeleted EventKind = "task_deleted"
	// EventProjectCreated — a project was created.
	EventProjectCreated EventKind = "project_created"
	// EventProjectUpdated — project fields changed (name, description, slug).
	EventProjectUpdated EventKind = "project_updated"
	// EventChatReply — an agent reply in a chat session (DM/GroupDirect).
	EventChatReply EventKind = "chat_reply"
	// EventReminderSummary — a periodic digest was generated.
	EventReminderSummary EventKind = "reminder_summary"
	// EventReset — the client must perform a cold resync (fell behind the retention window).
	EventReset EventKind = "reset"
	// EventChatMessageCreated — a chat message was created (user or agent) in a client session.
	EventChatMessageCreated EventKind = "chat_message_created"
	// EventVoiceFailed — async voice transcription permanently failed.
	EventVoiceFailed EventKind = "voice_failed"
)

type EventStore

type EventStore interface {
	// Insert saves an event within the given transaction and returns
	// the assigned seq value (monotonically increasing).
	Insert(ctx context.Context, tx *sql.Tx, ev Event) (int64, error)
	// SinceSeq returns events with seq > afterSeq in ascending seq order,
	// limited to limit records. limit <= 0 means no limit.
	SinceSeq(ctx context.Context, afterSeq int64, limit int) ([]Event, error)
	// MaxSeq returns the maximum seq in the table. Returns 0, nil for an empty table.
	MaxSeq(ctx context.Context) (int64, error)
	// MinSeq returns the minimum seq in the table. Used to distinguish a retention
	// gap from a natural AUTOINCREMENT gap (a rolled-back transaction loses its seq
	// without deleting an event). Returns 0, nil for an empty table.
	MinSeq(ctx context.Context) (int64, error)
	// GetBySeq returns an event by seq. Returns nil, nil if the event does not exist
	// (deleted by retention or never created).
	GetBySeq(ctx context.Context, seq int64) (*Event, error)
	// DeleteOlderThan deletes events with CreatedAt strictly before cutoff.
	// Returns the number of deleted rows.
	DeleteOlderThan(ctx context.Context, cutoff time.Time) (int64, error)
}

EventStore stores domain events (task/project/chat/...) published by use cases. Insert accepts an external transaction — the use case owns tx so it can atomically save entity changes together with the corresponding event and push_queue records. Read methods operate through the internal *sql.DB and require no transaction.

type Extractor

type Extractor interface {
	// Extract builds tasks from a message using dialog history context.
	// Returns an empty slice if no promises are found.
	Extract(ctx context.Context, msg Message, history []HistoryEntry) ([]Task, error)
}

Extractor extracts structured tasks from a promise message.

type ForwardBatchPayload added in v0.6.0

type ForwardBatchPayload struct {
	// ChatID is the DM chat identifier.
	ChatID string
	// Forwards is the list of forwarded messages loaded from the buffer.
	Forwards []ForwardEntry
	// TriggerText is the text of the user message that triggered processing
	// (empty when processing was triggered by a timeout sweeper).
	TriggerText string
	// TriggerID is the Telegram message ID of the trigger message
	// (empty when triggered by the sweeper).
	TriggerID string
	// DeleteFn deletes the given Telegram message IDs from the DM chat.
	// nil when delete_forwards_after_extract is false.
	DeleteFn func(ctx context.Context, ids []string) error
	// ReplyFn sends a reply to the DM chat.
	ReplyFn func(ctx context.Context, text string) error
	// Counterpart is the display name of the most frequent non-self sender in Forwards.
	// Empty when all forwards are self-authored or privacy is hidden for all.
	Counterpart string
}

ForwardBatchPayload carries a batch of forwarded messages for pipeline processing.

type ForwardBuffer added in v0.6.0

type ForwardBuffer interface {
	// Append adds a forward entry to the buffer. Duplicate (ChatID, MessageID) pairs are ignored.
	Append(ctx context.Context, e ForwardEntry) error
	// LastReceivedAt returns the maximum ReceivedAt timestamp for the given chat.
	// Returns zero time and false when the buffer is empty for that chat.
	LastReceivedAt(ctx context.Context, chatID string) (time.Time, bool, error)
	// LoadBatch returns all entries for the given chat ordered by OriginalDate ASC, id ASC.
	LoadBatch(ctx context.Context, chatID string) ([]ForwardEntry, error)
	// DeleteBatchTx deletes all entries for the given chat within the provided transaction.
	DeleteBatchTx(ctx context.Context, tx *sql.Tx, chatID string) error
	// ListPendingChats returns chat IDs whose MAX(received_at) is strictly before the given threshold.
	ListPendingChats(ctx context.Context, before time.Time) ([]string, error)
}

ForwardBuffer persists forwarded messages between receipt and batch processing. Append is idempotent — repeated calls with the same (ChatID, MessageID) are no-ops.

type ForwardDispatchSignal added in v0.6.0

type ForwardDispatchSignal struct {
	// ChatID is the DM chat identifier.
	ChatID string
	// TriggerText is the text of the user message that triggered processing
	// (empty when the sweeper initiates processing due to a timeout).
	TriggerText string
	// TriggerID is the Telegram message ID of the trigger message
	// (empty when initiated by the sweeper).
	TriggerID string
	// Source indicates what triggered dispatch: "user-trigger" or "sweeper".
	Source string
	// ReplyFn sends a reply to the DM chat.
	ReplyFn func(ctx context.Context, text string) error
	// DeleteFn deletes Telegram messages from the DM chat by their string IDs.
	// nil when delete_forwards_after_extract is false.
	DeleteFn func(ctx context.Context, ids []string) error
}

ForwardDispatchSignal is sent by TelegramChannel to the dispatcher goroutine to trigger processing of accumulated forwarded messages for a given DM chat.

type ForwardEntry added in v0.6.0

type ForwardEntry struct {
	// ChatID is the DM chat identifier.
	ChatID string
	// MessageID is the Telegram message identifier within the chat.
	MessageID string
	// ForwardFromID is the original sender's Telegram user ID (empty when privacy is hidden).
	ForwardFromID string
	// ForwardFromName is the display name of the original sender.
	ForwardFromName string
	// IsSelfAuthored is true when the forwarded message was authored by the bot owner.
	IsSelfAuthored bool
	// OriginalDate is the timestamp of the original message.
	OriginalDate time.Time
	// ReceivedAt is the time the forward was received in the DM.
	ReceivedAt time.Time
	// Text is the message body (empty for voice/media without a caption).
	Text string
}

ForwardEntry is a single forwarded message stored in the forward buffer.

type GuardPending

type GuardPending struct {
	ChatID       int64
	WelcomeMsgID int
	Deadline     time.Time
}

GuardPending represents a pending guard approval for a Telegram chat.

type GuardStore

type GuardStore interface {
	// UpsertPending saves or updates a pending approval record.
	UpsertPending(ctx context.Context, chatID int64, welcomeMsgID int, deadline time.Time) error
	// DeletePending removes the pending record for a chat (confirmed or manually cleared).
	DeletePending(ctx context.Context, chatID int64) error
	// ListPending returns all pending approval records.
	ListPending(ctx context.Context) ([]GuardPending, error)
}

GuardStore persists pending guard approvals so they survive service restarts.

type History

type History interface {
	// Add adds a record to the history for the given source channel.
	Add(ctx context.Context, source string, entry HistoryEntry) error

	// AddTx inserts a record within the caller-owned transaction and returns
	// the generated messageID (UUID v4). The caller is responsible for commit/rollback.
	// TTL and MaxMessages cleanup are NOT performed — use Add for standalone writes.
	AddTx(ctx context.Context, tx *sql.Tx, source string, entry HistoryEntry) (messageID string, err error)

	// Recent returns the last limit records from the given source channel.
	Recent(ctx context.Context, source string, limit int) ([]HistoryEntry, error)

	// RecentActivity returns records from the most recent activity wave:
	// looks for a stretch of records after a pause >= silenceGap.
	// If no such stretch is found, returns the last fallbackLimit records.
	RecentActivity(ctx context.Context, source string, silenceGap time.Duration, fallbackLimit int) ([]HistoryEntry, error)
}

History stores message history per channel.

type HistoryEntry

type HistoryEntry struct {
	// AuthorName is the author's display name.
	AuthorName string
	// Text is the message body.
	Text string
	// Timestamp is the time the message was created.
	Timestamp time.Time
}

HistoryEntry describes a single message history record stored in the store.

type Message

type Message struct {
	// ID is the unique message identifier within the channel.
	ID string
	// Source is the channel the message came from.
	Source Source
	// Author is the message author's identifier.
	Author string
	// AuthorName is the author's display name.
	AuthorName string
	// Subject is the message subject (populated for IMAP, empty for Telegram).
	Subject string
	// Text is the message body.
	Text string
	// Timestamp is the time the message was created.
	Timestamp time.Time
	// ReplyTo is the message being replied to (if any).
	ReplyTo *Message
	// Reaction is the user's reaction to the message (if any).
	Reaction *Reaction
	// Kind is the message type/context (DM, Batch, Group).
	Kind MessageKind
	// ReactFn is a callback for sending a reaction to the message (nil for non-Telegram sources).
	ReactFn func(ctx context.Context, emoji string) error
	// ReplyFn is a callback for sending a reply to the message (nil for non-Telegram sources).
	ReplyFn func(ctx context.Context, text string) error
	// HistoryFn is a callback for fetching the chat message history (nil if history is not needed).
	// Set by the channel for group messages; nil for DM and IMAP.
	HistoryFn func(ctx context.Context) ([]HistoryEntry, error)
	// ForwardBatch carries the forward batch payload; non-nil only when Kind == MessageKindForwardBatch.
	ForwardBatch *ForwardBatchPayload
	// ProjectHint is an optional project context hint from the client. When non-nil, the agent
	// uses it as the default project for new tasks if the message does not explicitly name another.
	// Set only for client voice jobs that include a projectId in the request metadata.
	ProjectHint *Project
}

Message describes a single message from a channel.

type MessageKind

type MessageKind string

MessageKind describes the type/context of a message.

const (
	// MessageKindDM is a direct message from the owner to the bot.
	MessageKindDM MessageKind = "dm"
	// MessageKindBatch is a message from a batch source (IMAP).
	MessageKindBatch MessageKind = "batch"
	// MessageKindGroup is a message from a group chat.
	MessageKindGroup MessageKind = "group"
	// MessageKindGroupDirect is a bot-directed message in a group chat (mention or reply to bot).
	MessageKindGroupDirect MessageKind = "group_direct"
	// MessageKindForwardBatch is a batch of forwarded messages from a 1:1 Telegram chat.
	MessageKindForwardBatch MessageKind = "forward_batch"
)

type MetaStore

type MetaStore interface {
	// Get returns the value for the given key. Returns "", nil if the key is not found.
	Get(ctx context.Context, key string) (string, error)
	// SetTx sets the value for the given key within the given transaction.
	SetTx(ctx context.Context, tx *sql.Tx, key, value string) error
	// Values returns all values whose keys start with the given prefix.
	// Returns nil, nil if no matching keys are found.
	Values(ctx context.Context, prefix string) ([]string, error)
}

MetaStore stores arbitrary channel metadata as key-value pairs.

Write methods accept an external transaction (*sql.Tx) — the use-case layer owns the transaction so it can atomically write metadata together with other stores. Read methods have no tx and go through the internal *sql.DB.

type Notifier

type Notifier interface {
	// Notify sends a notification about a batch of tasks.
	Notify(ctx context.Context, tasks []Task) error
	// Name returns the human-readable notifier name for logging.
	Name() string
}

Notifier sends the user a notification about new tasks.

type PairingRequest

type PairingRequest struct {
	// DeviceName is the human-readable device name (e.g. "iPhone 17").
	DeviceName string
	// Platform is the device platform: ios|android|macos|windows|linux.
	Platform string
	// ClientNonce is a random string from the client; stored as its sha256 hash.
	ClientNonce string
	// APNSToken is the APNs push token (optional).
	APNSToken *string
	// FCMToken is the FCM push token (optional).
	FCMToken *string
}

PairingRequest holds input parameters for a device pairing request.

type PairingResult

type PairingResult struct {
	// PairID is the request UUID.
	PairID string
	// Status is the current status.
	Status PairingStatus
	// DeviceID is the device UUID; populated only when Status==PairingStatusConfirmed.
	DeviceID string
	// BearerToken is the device bearer token; shown ONCE via the broadcaster.
	BearerToken string
}

PairingResult is the result of polling a pairing request status.

type PairingService

type PairingService interface {
	// RequestPairing creates a new device pairing request, saves it to the store,
	// and sends the owner a DM with the magic link.
	RequestPairing(ctx context.Context, req PairingRequest) (*PendingPairing, error)
	// PollStatus waits for the pairing request result (long-poll up to longPollTTL).
	// Verifies sha256(clientNonce) against the stored NonceHash.
	// Returns PairingResult with Status=="pending" on timeout or Status=="confirmed" on success.
	PollStatus(ctx context.Context, pairID, clientNonce string) (*PairingResult, error)
	// PrepareConfirm generates and saves SHA256(csrfToken) for a pairing request.
	// Used by the GET /pair/confirm/{id} HTML handler before rendering the form.
	PrepareConfirm(ctx context.Context, pairID, csrfToken string) (*PendingPairing, error)
	// ConfirmWithCSRF confirms the pairing: verifies the CSRF token, creates the device,
	// and publishes the bearer token via the broadcaster.
	ConfirmWithCSRF(ctx context.Context, pairID, csrfToken string) (*Device, error)
}

PairingService is the use-case layer for the pairing flow (connecting a device via magic link).

type PairingStatus

type PairingStatus string

PairingStatus describes the current state of a pairing request.

const (
	// PairingStatusPending — the request is awaiting owner confirmation.
	PairingStatusPending PairingStatus = "pending"
	// PairingStatusConfirmed — the request was confirmed and the device registered.
	PairingStatusConfirmed PairingStatus = "confirmed"
	// PairingStatusExpired — the request expired without confirmation.
	PairingStatusExpired PairingStatus = "expired"
)

type PairingStore

type PairingStore interface {
	// CreateTx creates a new pairing request within the given transaction.
	// Populates CreatedAt; all other fields must be set by the caller.
	CreateTx(ctx context.Context, tx *sql.Tx, p *PendingPairing) error
	// Get returns a request by UUID. Returns nil, nil if not found.
	Get(ctx context.Context, id string) (*PendingPairing, error)
	// SetCSRFTx saves the SHA256 hash of the CSRF token within the given transaction.
	SetCSRFTx(ctx context.Context, tx *sql.Tx, id, csrfHash string) error
	// MarkConfirmedTx marks the request as confirmed and records the deviceID within the given transaction.
	MarkConfirmedTx(ctx context.Context, tx *sql.Tx, id, deviceID string) error
	// DeleteExpired deletes records whose expires_at < cutoff.
	// Returns the number of deleted rows.
	DeleteExpired(ctx context.Context, cutoff time.Time) (int64, error)
}

PairingStore manages pairing request records in the store.

Write methods (CreateTx, SetCSRFTx, MarkConfirmedTx) accept an external transaction — the use-case layer owns tx to atomically write the pairing record together with the device. Read methods and DeleteExpired operate without a transaction.

type PendingPairing

type PendingPairing struct {
	// ID is the request UUID; used in the magic-link URL.
	ID string
	// DeviceName is the device name from the request.
	DeviceName string
	// Platform is the device platform from the request.
	Platform string
	// APNSToken is the APNs push token (nil if not provided).
	APNSToken *string
	// FCMToken is the FCM push token (nil if not provided).
	FCMToken *string
	// NonceHash is the hex-encoded SHA256 of ClientNonce.
	NonceHash string
	// CSRFHash is the hex-encoded SHA256 of the CSRF token; empty until generated.
	CSRFHash string
	// CreatedAt is the time the request was created.
	CreatedAt time.Time
	// ExpiresAt is the time the request expires (the magic link is invalid after this).
	ExpiresAt time.Time
	// ConfirmedAt is the time the owner confirmed the request (nil if not yet confirmed).
	ConfirmedAt *time.Time
	// IssuedDeviceID is the UUID of the created device (nil before confirmation).
	IssuedDeviceID *string
}

PendingPairing describes a pairing request record in the store.

type Project

type Project struct {
	// ID is the project UUID.
	ID string
	// Name is the project name (unique).
	Name string
	// Slug is the lowercase-kebab project identifier (unique).
	Slug string
	// Description is the project description.
	Description string
	// Aliases is the list of keyword aliases for this project, sorted lexicographically.
	// Always non-nil; empty slice if no aliases are defined.
	Aliases []string
	// TaskCounter is a monotonic task counter; not rolled back on move.
	TaskCounter int
	// CreatedAt is the time the project was created.
	CreatedAt time.Time
}

Project describes a project — a container for tasks.

type ProjectGroup

type ProjectGroup struct {
	// ProjectID is the project UUID.
	ProjectID string
	// ProjectName is the project name.
	ProjectName string
	// Tasks are the project's tasks in this section.
	Tasks []Task
}

ProjectGroup holds tasks for a single project within a digest section.

type ProjectService

type ProjectService interface {
	// CreateProject creates a new project; generates a slug if not provided.
	CreateProject(ctx context.Context, req CreateProjectRequest) (*Project, error)
	// UpdateProject applies changes to a project.
	UpdateProject(ctx context.Context, id string, upd ProjectUpdate) (*Project, error)
	// ListProjects returns all projects.
	ListProjects(ctx context.Context) ([]Project, error)
	// FindProjectByName finds a project by name. Returns nil, nil if not found.
	FindProjectByName(ctx context.Context, name string) (*Project, error)
	// ResolveProjectForChannel returns the UUID of the project bound to the given channel.
	// If no mapping is found, returns the Inbox ID.
	ResolveProjectForChannel(ctx context.Context, channelID string) (string, error)
	// EnsureChannelProject idempotently creates a project and binds it to the channel.
	EnsureChannelProject(ctx context.Context, channelID, name string) (*Project, error)
	// AddProjectAlias adds an alias to the project and returns the updated project.
	AddProjectAlias(ctx context.Context, projectID, alias string) (*Project, error)
	// RemoveProjectAlias removes an alias from the project and returns the updated project.
	RemoveProjectAlias(ctx context.Context, projectID, alias string) (*Project, error)
	// ResolveProjectRef finds a project by UUID, slug, or alias.
	// Returns ErrProjectNotFound if no match is found.
	ResolveProjectRef(ctx context.Context, ref string) (*Project, error)
}

ProjectService is the use-case layer for project operations.

type ProjectUpdate

type ProjectUpdate struct {
	// Name is the new project name (nil means no change).
	Name *string
	// Description is the new description (nil means no change).
	Description *string
	// Slug is the new project slug (nil means no change).
	Slug *string
	// Aliases is the replacement set of aliases (nil means no change; &[]string{} clears all aliases).
	Aliases *[]string
}

ProjectUpdate describes changes to apply to a project.

type PushJob

type PushJob struct {
	// ID is the auto-increment push_queue row identifier.
	ID int64
	// DeviceID is the target device.
	DeviceID string
	// EventSeq is the seq of the event to deliver.
	EventSeq int64
	// CreatedAt is the time the job was enqueued.
	CreatedAt time.Time
	// Attempts is the number of delivery attempts already made.
	Attempts int
	// LastError is the error text from the last failed attempt (empty if none).
	LastError string
	// NextAttemptAt is the earliest time the next attempt should be made.
	NextAttemptAt time.Time
}

PushJob describes a single row in the push notification queue. A job is created by the use case for inactive (offline) devices and later processed by the push relay dispatcher. The queue is populated but not consumed until the dispatcher is running.

type PushQueue

type PushQueue interface {
	// Enqueue adds a single job to the queue within the given transaction.
	// Sets CreatedAt to now, NextAttemptAt = CreatedAt, Attempts = 0.
	Enqueue(ctx context.Context, tx *sql.Tx, deviceID string, eventSeq int64) error
	// NextBatch returns up to limit undelivered and non-dropped jobs
	// with NextAttemptAt <= now, sorted by NextAttemptAt ASC, id ASC.
	NextBatch(ctx context.Context, limit int) ([]PushJob, error)
	// MarkDelivered marks a job as successfully delivered (sets delivered_at = now).
	MarkDelivered(ctx context.Context, id int64) error
	// MarkFailed increments attempts, stores the error text, and schedules
	// the next retry no earlier than nextAttempt.
	MarkFailed(ctx context.Context, id int64, errText string, nextAttempt time.Time) error
	// Drop marks a job as permanently failed (dropped_at = now) with the given reason.
	Drop(ctx context.Context, id int64, reason string) error
	// DeleteDelivered removes delivered or dropped jobs whose completion
	// (delivered_at or dropped_at) occurred before cutoff.
	// Returns the number of deleted rows.
	DeleteDelivered(ctx context.Context, cutoff time.Time) (int64, error)
}

PushQueue stores the push job queue for devices not connected to SSE at the time an event is published. The use case enqueues a job within the same transaction as the event insert — therefore Enqueue accepts *sql.Tx. Other methods read/update the queue outside a transaction and are used by the push dispatcher and retention goroutine.

type Reaction

type Reaction struct {
	// Emoji is the reaction symbol (e.g. "👍").
	Emoji string
	// UserID is the identifier of the user who left the reaction.
	UserID string
}

Reaction describes an emoji reaction to a message.

type Source

type Source struct {
	// Kind is the channel type: "telegram" or "imap".
	Kind string
	// ID is the unique channel identifier (e.g. chat_id or email address).
	ID string
	// Name is the human-readable channel name.
	Name string
	// AccountID is the identifier of the watcher that created the message (e.g. "work", "personal").
	// Empty string for IMAP sources.
	AccountID string
}

Source describes the origin of a message (the channel).

type StateStore

type StateStore interface {
	// GetCursor returns the saved cursor for a channel.
	// Returns nil, nil if no cursor is found.
	GetCursor(ctx context.Context, channelID string) (*Cursor, error)

	// SaveCursor saves the cursor for a channel.
	SaveCursor(ctx context.Context, channelID string, cursor Cursor) error
}

StateStore persists and restores the read position per channel.

type Summary

type Summary struct {
	// GeneratedAt is the time the digest was generated.
	GeneratedAt time.Time
	// Slot is the schedule slot: "morning", "afternoon", or "evening".
	Slot string
	// Overdue is the list of overdue tasks, grouped by project.
	Overdue []ProjectGroup
	// Today is the list of tasks due today, grouped by project.
	Today []ProjectGroup
	// Upcoming is the list of tasks due within the planning horizon, grouped by project.
	Upcoming []ProjectGroup
	// Undated is the list of tasks with no deadline, already trimmed to UndatedLimit.
	Undated []ProjectGroup
	// UndatedTotal is the total count of undated tasks (before trimming).
	UndatedTotal int
	// IsEmpty is true when all sections are empty.
	IsEmpty bool
}

Summary holds the content of a periodic digest, grouped into sections.

type SummaryDeliverer

type SummaryDeliverer interface {
	// Deliver sends the digest to the recipient.
	Deliver(ctx context.Context, summary Summary) error
	// Name returns the human-readable deliverer name for logging.
	Name() string
}

SummaryDeliverer delivers a task digest to a single channel (Telegram, email, ...).

type Task

type Task struct {
	// ID is the task UUID.
	ID string
	// Number is the per-project monotonic task number.
	Number int
	// ProjectID is the UUID of the project the task belongs to.
	ProjectID string
	// ProjectSlug is the project slug; populated by the store via JOIN on SELECT.
	ProjectSlug string
	// Summary is a short description of what needs to be done.
	Summary string
	// Details is the task context or details (populated by the extractor).
	Details string
	// Topic is the task's thematic group (e.g. "Deploy", "iOS Client").
	// Populated by the extractor; cleared for group chats in the pipeline.
	Topic string
	// Status is the task status: "open", "done", or "cancelled".
	Status string
	// Deadline is the task due date (nil if not specified).
	Deadline *time.Time
	// Confidence is the model's confidence in the extracted task (0.0–1.0).
	Confidence float64
	// Source is the technical identifier of the source channel.
	Source Source
	// SourceMessage is the original message containing the promise.
	// Contains Subject (email subject for IMAP) and ReactFn/ReplyFn callbacks.
	SourceMessage Message
	// CreatedAt is the time the task record was created.
	CreatedAt time.Time
	// UpdatedAt is the time the task was last updated.
	UpdatedAt time.Time
	// ClosedAt is the time the task was moved to done or cancelled (nil for open tasks).
	ClosedAt *time.Time
}

Task describes a task extracted from a user promise.

func (Task) DisplayID

func (t Task) DisplayID() string

DisplayID returns a human-readable reference like "inbox#42".

type TaskFilter

type TaskFilter struct {
	// Status restricts the result set by status: "open", "done", or "cancelled".
	// Empty string means all statuses.
	Status string
	// Query is a substring to search in the task summary (case-insensitive LIKE).
	// Empty string means no text filter.
	Query string
	// Limit is the maximum number of tasks to return (0 means no limit).
	Limit int
}

TaskFilter specifies task filtering criteria.

type TaskService

type TaskService interface {
	// CreateTask creates a single task; populates ID, Number, CreatedAt, UpdatedAt, Status.
	CreateTask(ctx context.Context, req CreateTaskRequest) (*Task, error)
	// CreateTasks creates a batch of tasks for a single project.
	CreateTasks(ctx context.Context, req CreateTasksRequest) ([]Task, error)
	// UpdateTask applies changes to a task.
	UpdateTask(ctx context.Context, id string, upd TaskUpdate) (*Task, error)
	// CompleteTask moves a task to "done" status.
	CompleteTask(ctx context.Context, id string) (*Task, error)
	// ReopenTask moves a task to "open" status.
	ReopenTask(ctx context.Context, id string) (*Task, error)
	// MoveTask moves a task to another project, reassigning its number.
	MoveTask(ctx context.Context, id, newProjectID string) (*Task, error)
	// ListTasks returns tasks for a project matching the filter.
	// projectID="" means all projects.
	ListTasks(ctx context.Context, projectID string, filter TaskFilter) ([]Task, error)
	// GetTask returns a task by UUID.
	GetTask(ctx context.Context, id string) (*Task, error)
	// GetTaskByRef finds a task by its human-readable reference "<slug>#<number>".
	GetTaskByRef(ctx context.Context, projectSlug string, number int) (*Task, error)
	// PurgeClosedOlderThan deletes done/cancelled tasks closed before cutoff
	// and emits a task_deleted event for each one (transactionally with the
	// delete). Returns the number of tasks deleted.
	PurgeClosedOlderThan(ctx context.Context, cutoff time.Time) (int, error)
}

TaskService is the use-case layer for task operations.

type TaskStore

type TaskStore interface {
	// CreateProjectTx creates a new project within the given transaction. Requires p.Slug != "".
	// Returns an error on name duplication.
	CreateProjectTx(ctx context.Context, tx *sql.Tx, project *Project) error
	// UpdateProjectTx applies changes to a project within the given transaction.
	UpdateProjectTx(ctx context.Context, tx *sql.Tx, id string, upd ProjectUpdate) error
	// GetProject returns a project by UUID. Returns nil, nil if not found.
	GetProject(ctx context.Context, id string) (*Project, error)
	// GetProjectTx reads a project by UUID within the given transaction.
	// Needed by the use-case layer when it needs to read the current state
	// after UpdateProjectTx without releasing the single SQLite connection.
	GetProjectTx(ctx context.Context, tx *sql.Tx, id string) (*Project, error)
	// ListProjects returns all projects.
	ListProjects(ctx context.Context) ([]Project, error)
	// FindProjectByName finds a project by name. Returns nil, nil if not found.
	FindProjectByName(ctx context.Context, name string) (*Project, error)
	// CreateTaskTx creates a new task within the given transaction; sets Number, ID,
	// CreatedAt, UpdatedAt, Status="open".
	CreateTaskTx(ctx context.Context, tx *sql.Tx, task *Task) error
	// GetTask returns a task by UUID. Returns nil, nil if not found.
	GetTask(ctx context.Context, id string) (*Task, error)
	// GetTaskTx reads a task by UUID within the given transaction.
	// Needed by the use-case layer to read the current state after
	// UpdateTaskTx/MoveTaskTx without releasing the single SQLite connection.
	GetTaskTx(ctx context.Context, tx *sql.Tx, id string) (*Task, error)
	// GetTaskByRef finds a task by its human-readable reference "<slug>#<number>".
	// Returns nil, nil if not found.
	GetTaskByRef(ctx context.Context, projectSlug string, number int) (*Task, error)
	// ListTasks returns tasks matching the filter.
	// projectID="" means all projects.
	ListTasks(ctx context.Context, projectID string, filter TaskFilter) ([]Task, error)
	// UpdateTaskTx applies changes to a task within the given transaction.
	UpdateTaskTx(ctx context.Context, tx *sql.Tx, id string, update TaskUpdate) error
	// MoveTaskTx moves a task to another project within the given transaction,
	// assigning it a new number.
	MoveTaskTx(ctx context.Context, tx *sql.Tx, taskID, newProjectID string) error
	// DefaultProjectID returns the UUID of the "Inbox" project.
	DefaultProjectID() string
	// AddProjectAliasTx inserts an alias for the project within the given transaction.
	// Returns an error if the alias PRIMARY KEY already exists (alias taken by any project)
	// or if projectID does not reference an existing project (FK violation).
	AddProjectAliasTx(ctx context.Context, tx *sql.Tx, projectID, alias string) error
	// RemoveProjectAliasTx deletes an alias within the given transaction.
	// Returns nil when the alias does not exist (callers pre-validate membership).
	RemoveProjectAliasTx(ctx context.Context, tx *sql.Tx, projectID, alias string) error
	// ListAliasesForProject returns the aliases for a project sorted lexicographically.
	// Returns an empty slice when the project has no aliases.
	ListAliasesForProject(ctx context.Context, projectID string) ([]string, error)
	// ListClosedOlderThan returns full snapshots of tasks with status
	// "done"/"cancelled" whose closed_at is strictly before cutoff. Used by the
	// retention purge so the use-case can emit task_deleted events with the
	// task payload before deleting the rows.
	ListClosedOlderThan(ctx context.Context, cutoff time.Time) ([]Task, error)
	// DeleteTaskTx physically deletes a task row by id within the given
	// transaction. Returns nil even when no row matches — callers pre-validate
	// existence via the corresponding read.
	DeleteTaskTx(ctx context.Context, tx *sql.Tx, id string) error
}

TaskStore stores user projects and tasks.

Write methods accept an external transaction (*sql.Tx) — the use-case layer owns the transaction so it can atomically write to multiple stores (events, push_queue) together with task or project changes. Read methods have no tx and go through the internal *sql.DB.

type TaskUpdate

type TaskUpdate struct {
	// Summary is the new short description (nil means no change).
	Summary *string
	// Details is the new task details (nil means no change).
	Details *string
	// Topic is the new thematic group (nil means no change).
	Topic *string
	// Status is the new task status (nil means no change).
	Status *string
	// Deadline is the new due date (nil means no change; pointer to nil clears the deadline).
	Deadline **time.Time
	// ClosedAt is the task close time (nil means no change; pointer to nil clears the field).
	ClosedAt **time.Time
}

TaskUpdate describes changes to apply to a task.

type Transcriber added in v0.4.0

type Transcriber interface {
	// Transcribe returns the recognized text for the given audio payload.
	// An empty string with nil error indicates the recognizer ran successfully but
	// produced no text (silence/noise).
	Transcribe(ctx context.Context, audio AudioInput) (string, error)
	// Name returns the human-readable transcriber name for logging.
	Name() string
}

Transcriber converts a voice/audio recording to plain text. Implementations live in internal/transcribe (WhisperCPP, Yandex).

type VoiceConsumer added in v0.7.0

type VoiceConsumer interface {
	Consume(ctx context.Context, job VoiceJob, transcript string) error
	Fail(ctx context.Context, job VoiceJob, cause error)
}

VoiceConsumer handles the result of voice transcription for a specific source kind. Consume is called on success; Fail is called when the job is permanently dropped (max attempts reached) or when transcription produces an empty result (cause = ErrEmptyTranscript).

type VoiceDownloader added in v0.7.0

type VoiceDownloader interface {
	Download(ctx context.Context, job VoiceJob) ([]byte, error)
}

VoiceDownloader downloads the raw audio bytes for a voice job. Returns []byte for direct use with Transcriber.AudioInput.Data. The caller limits the read to voice_max_bytes via MaxBytesReader before passing data here; implementations may impose their own limit as well.

type VoiceJob added in v0.6.1

type VoiceJob struct {
	// ID is the auto-increment voice_queue row identifier (0 before insertion).
	ID int64
	// SourceKind identifies the originating channel: "telegram" or "client".
	SourceKind string
	// Payload is the audio locator: file_id for telegram, relative path for client.
	Payload string
	// MimeType is the audio MIME type (e.g. "audio/ogg").
	MimeType string
	// Duration is the audio length.
	Duration time.Duration
	// ReceivedAt is the time the message was originally received.
	ReceivedAt time.Time
	// Attempts is the number of processing attempts already made.
	Attempts int
	// NextAttemptAt is the scheduled time for the next processing attempt.
	NextAttemptAt time.Time
	// LastError holds the most recent processing error message, if any.
	LastError string

	// Telegram-only fields:
	ChatID    int64
	MessageID int

	// Client-only fields:
	DeviceID    string
	ClientJobID string // UUID v4 returned to the client in the 202 response
	ProjectID   string // optional project context hint (UUID)
}

VoiceJob represents a single voice transcription task persisted in voice_queue.

type VoiceQueue added in v0.6.1

type VoiceQueue interface {
	// Enqueue inserts a job into the queue. Duplicate (ChatID, MessageID) pairs are ignored.
	Enqueue(ctx context.Context, job VoiceJob) error
	// Claim returns the first job whose next_attempt_at <= now. Returns nil, nil
	// when the queue is empty or no job is due yet. The job remains in the queue
	// until the caller calls Complete or Reschedule.
	Claim(ctx context.Context, now time.Time) (*VoiceJob, error)
	// Complete permanently removes a job (successful processing or final drop).
	Complete(ctx context.Context, id int64) error
	// Reschedule increments attempts, records the last error, and sets the next
	// attempt time. Used for retryable failures.
	Reschedule(ctx context.Context, id int64, nextAt time.Time, lastErr string) error
}

VoiceQueue persists voice transcription jobs between receipt and async processing. Enqueue is idempotent — repeated calls with the same (ChatID, MessageID) are no-ops.

Jump to

Keyboard shortcuts

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