thread

package
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
const (
	TypeChat              = "chat"
	TypeSchedule          = "schedule"
	TypeSubagent          = "subagent"
	TypeDiscuss           = "discuss"
	TypeACPAgent          = "acp_agent"
	RuntimeModel          = string(runtimekind.Model)
	RuntimeACPAgent       = string(runtimekind.ACPAgent)
	RuntimeCodex          = string(runtimekind.Codex)
	RuntimeClaudeCode     = string(runtimekind.ClaudeCode)
	DefaultACPProjectMode = "project"
	DefaultACPProjectPath = "/data"
)

The runtime vocabulary is owned by runtimekind (a leaf package shared with consumers that cannot import this one); these constants are pinned aliases so the session domain keeps its established names.

Variables

View Source
var (
	ErrACPAgentIDRequired     = errors.New("acp_agent_id is required for acp_agent sessions")
	ErrACPProjectPathMissing  = errors.New("project_path is required for acp_agent sessions")
	ErrACPUnknownAgent        = errors.New("unknown ACP agent")
	ErrACPAgentNotEnabled     = errors.New("ACP agent is not enabled for this bot")
	ErrACPAgentNotConfigured  = errors.New("ACP agent is not configured for this bot")
	ErrACPRuntimeOwnerMissing = errors.New("runtime_owner_account_id is required for acp_agent sessions")
	ErrACPProjectModeInvalid  = errors.New("unknown ACP project mode")
	// ErrExternalRuntimeOwnerMissing mirrors the ACP owner requirement for
	// direct external runtimes: turns execute with workspace authority, so a
	// session without an owner has nobody to authorize them against.
	ErrExternalRuntimeOwnerMissing = errors.New("runtime_owner_account_id is required for external runtime sessions")
	ErrForkSourceNotFound          = errors.New("fork source session not found")
	ErrForkSourceNotReply          = errors.New("fork source must be a visible assistant reply")
	ErrForkSourceNotChat           = errors.New("fork source must be a chat session")
	ErrSessionHasMessages          = errors.New("session has visible messages")
)
View Source
var ErrRuntimeMetadataStale = errors.New("session runtime changed; runtime metadata merge skipped")

ErrRuntimeMetadataStale reports that a runtime-metadata merge was skipped because the session no longer runs the expected runtime.

Functions

func AllSessionTypes

func AllSessionTypes() []string

AllSessionTypes returns every known session type. The paged list queries always demand an explicit type filter; callers that filter by visibility instead pass this list to make the type predicate a no-op.

func ApplyACPMetadataDefaults

func ApplyACPMetadataDefaults(meta map[string]any) map[string]any

ApplyACPMetadataDefaults fills omitted ACP session project fields.

func ApplyExternalMetadataDefaults added in v0.20.0

func ApplyExternalMetadataDefaults(meta map[string]any) map[string]any

ApplyExternalMetadataDefaults fills omitted working-directory metadata for direct external runtime sessions. Unlike ACP there is no project mode: the runtime always works inside the bot workspace.

func DescriptorFromLegacyType

func DescriptorFromLegacyType(typ string) (string, string)

DescriptorFromLegacyType maps the legacy single type column to the split session mode/runtime descriptor used by new code paths.

func IsACPRuntime

func IsACPRuntime(thread Thread) bool

IsACPRuntime reports whether a session is backed by an ACP runtime. It keeps legacy chat ACP sessions (`type=acp_agent`) working while allowing newer descriptors such as `session_mode=discuss` + `runtime_type=acp_agent`.

func IsDirectRuntime added in v0.20.0

func IsDirectRuntime(thread Thread) bool

IsDirectRuntime reports whether a session runs on a direct external agent runtime.

func IsDirectRuntimeType added in v0.20.0

func IsDirectRuntimeType(runtimeType string) bool

IsDirectRuntimeType reports whether a runtime type is one of the direct external agent runtimes, outside the ACP compatibility pool.

func IsKnownRuntimeType

func IsKnownRuntimeType(runtimeType string) bool

func IsKnownSessionMode

func IsKnownSessionMode(mode string) bool

func IsKnownType

func IsKnownType(typ string) bool

func IsUserFacingType

func IsUserFacingType(typ string) bool

IsUserFacingType reports whether a session type is one that user-facing session list endpoints should return by default.

func LegacyTypeForDescriptor

func LegacyTypeForDescriptor(sessionMode, runtimeType string) string

LegacyTypeForDescriptor returns the compatibility type value for a split session descriptor.

func ResolveDescriptor

func ResolveDescriptor(legacyType, sessionMode, runtimeType string) (string, string, string, error)

ResolveDescriptor returns the normalized compatibility type plus split session-mode/runtime descriptor without applying metadata side effects.

func SupportsSkillActivation

func SupportsSkillActivation(sessionMode, legacyType, runtimeType string) bool

SupportsSkillActivation reports whether a session shape can accept user-requested skill activation: chat mode (with the usual legacy-type fallback) on the built-in model runtime. This is the single definition for the rule — the web WS pre-check, the channel inbound gate, and the flow resolver's final guard all call it, so the three surfaces cannot drift.

func UserFacingSessionTypes

func UserFacingSessionTypes() []string

UserFacingSessionTypes returns a fresh copy of the user-facing session type list so callers can read or mutate it without disturbing the package-level source of truth.

func UsesDecisionWaiter added in v0.20.0

func UsesDecisionWaiter(thread Thread) bool

UsesDecisionWaiter reports whether tool-approval and user-input decisions for this session are answered by waking an in-process runtime waiter (the ACP pool or a direct external runtime) instead of resuming the native agent loop with a tool result.

Types

type ACPSetupValidation

type ACPSetupValidation struct {
	Known                 bool
	Enabled               bool
	MissingManagedFieldID string
}

ACPSetupValidation is the channel- and runtime-independent policy result needed before a thread can persist an ACP runtime descriptor.

type ACPSetupValidator

type ACPSetupValidator interface {
	ValidateACPSetup(agentID string, botMetadata map[string]any) ACPSetupValidation
}

ACPSetupValidator is implemented by an Agent adapter. Thread owns this port so chat persistence never imports an Agent runtime implementation.

type CreateInput

type CreateInput struct {
	BotID           string
	BotAgentID      string
	RouteID         string
	ChannelType     string
	Type            string
	SessionMode     string
	RuntimeType     string
	Title           string
	Metadata        map[string]any
	RuntimeMetadata map[string]any
	ParentThreadID  string
	CreatedByUserID string
	// WorkdirID immutably binds the thread to a bot workdir. The caller
	// (handler layer) validates the workdir; thread persistence only stores
	// the reference so this package never imports the workdir domain.
	WorkdirID string
	// WorkdirPath is the resolved workdir directory, set if and only if
	// WorkdirID is set. For ACP threads it overrides the metadata
	// project_path so the runtime works in that directory; it also
	// leaves a creation-time path snapshot in the metadata for history.
	WorkdirPath string
	// PreferredChatModelID / PreferredReasoningEffort are the first-message
	// pair written at INSERT time (issue #879, spec §3.3-D) so the
	// session_created broadcast never precedes the value. Empty = NULL.
	// Non-UUID model references degrade to NULL; the steady-state
	// write-back completes the pair on the same turn.
	PreferredChatModelID     string
	PreferredReasoningEffort string
	// Visibility overrides the default visibility derived from the session
	// mode. Schedule-created sessions use this to surface in user-facing
	// session lists while keeping session_mode='schedule' for prompt and
	// tool gating. Empty means "derive from mode".
	Visibility Visibility
}

CreateInput holds input for creating a new thread.

type CreateSubagentInput

type CreateSubagentInput struct {
	Thread       CreateInput
	ModelUUID    string
	ModelID      string
	ProviderName string
	Forked       bool
	ForkContext  []SubagentForkContextMessage
}

type Cursor

type Cursor struct {
	UpdatedAt time.Time
	ID        string
}

Cursor identifies the position in a thread listing for keyset pagination. The zero value means "start from the head".

func (Cursor) IsZero

func (c Cursor) IsZero() bool

IsZero reports whether the cursor carries neither half — the start-of-list signal that pagedCursorParams maps to "no cursor predicate". A partially-populated cursor (only the timestamp or only the id) is not zero; pagedCursorParams rejects it as a programmer error so we never send malformed bindings down to SQL.

type ForkFromAssistantInput

type ForkFromAssistantInput struct {
	BotID    string
	ThreadID string
	// RuntimeMetadataOverride replaces the clone's runtime metadata. External
	// runtime forks must pass it carrying the driver's freshly forked session
	// keys; without it the two Memoh sessions would share one runtime session.
	RuntimeMetadataOverride map[string]any
	// TurnID names the round the fork inherits through. A turn is the identity
	// a client holds while the round is still live, and the cut is turn-level
	// anyway, so the fork point is named by turn rather than by stored message.
	TurnID string
	// MessageID is the pre-turn spelling of TurnID.
	//
	// Deprecated: accepted so a client shipped against the message-id contract
	// keeps working after a server upgrade. It is resolved to the round that
	// contains it. Remove once the compatibility window closes.
	MessageID       string
	Title           string
	CreatedByUserID string
}

ForkFromAssistantInput creates a new chat thread from the source thread's visible history through the assistant message's turn.

type ListFilter

type ListFilter struct {
	ParentThreadID string
	// WorkdirID filters to sessions bound to one workdir. Mutually
	// exclusive with WorkdirUnassigned.
	WorkdirID string
	// WorkdirUnassigned filters to sessions with no workdir binding — the
	// sidebar's ungrouped bucket.
	WorkdirUnassigned bool
	// Visibility filters to sessions with the given stored visibility.
	// Empty means no visibility predicate. The default session listing
	// passes VisibilityUser so schedule-created sessions surface by
	// visibility rather than by widening the legacy type filter.
	Visibility Visibility
}

type Queries

type Queries interface {
	CountMessagesBySession(context.Context, pgtype.UUID) (int64, error)
	CreateSession(context.Context, sqlc.CreateSessionParams) (sqlc.BotSession, error)
	CreateSubagentConfig(context.Context, sqlc.CreateSubagentConfigParams) (sqlc.SubagentConfig, error)
	CreateSubagentForkContext(context.Context, sqlc.CreateSubagentForkContextParams) (sqlc.CreateSubagentForkContextRow, error)
	ForkSessionFromAssistantTurn(context.Context, sqlc.ForkSessionFromAssistantTurnParams) (sqlc.ForkSessionFromAssistantTurnRow, error)
	GetVisibleHistoryTurnByMessage(context.Context, sqlc.GetVisibleHistoryTurnByMessageParams) (dbstore.HistoryTurn, error)
	GetBotByID(context.Context, pgtype.UUID) (sqlc.GetBotByIDRow, error)
	GetSessionByID(context.Context, pgtype.UUID) (sqlc.BotSession, error)
	GetLatestSessionModelPreference(context.Context, sqlc.GetLatestSessionModelPreferenceParams) (sqlc.GetLatestSessionModelPreferenceRow, error)
	GetSubagentConfig(context.Context, pgtype.UUID) (sqlc.SubagentConfig, error)
	ListSessionsByBot(context.Context, pgtype.UUID) ([]sqlc.ListSessionsByBotRow, error)
	ListSessionsByBotAndCreatedByUser(context.Context, sqlc.ListSessionsByBotAndCreatedByUserParams) ([]sqlc.ListSessionsByBotAndCreatedByUserRow, error)
	ListSessionsByBotAndCreatedByUserPaged(context.Context, sqlc.ListSessionsByBotAndCreatedByUserPagedParams) ([]sqlc.ListSessionsByBotAndCreatedByUserPagedRow, error)
	ListSessionsByBotPaged(context.Context, sqlc.ListSessionsByBotPagedParams) ([]sqlc.ListSessionsByBotPagedRow, error)
	ListSessionsByRoute(context.Context, pgtype.UUID) ([]sqlc.BotSession, error)
	ListSubagentForkContext(context.Context, pgtype.UUID) ([]sqlc.ListSubagentForkContextRow, error)
	ListSubagentSessionsByParent(context.Context, pgtype.UUID) ([]sqlc.BotSession, error)
	SoftDeleteSession(context.Context, pgtype.UUID) error
	TouchSession(context.Context, pgtype.UUID) error
	UpdateSessionMetadata(context.Context, sqlc.UpdateSessionMetadataParams) (sqlc.BotSession, error)
	UpdateSessionRuntimeMetadata(context.Context, sqlc.UpdateSessionRuntimeMetadataParams) (sqlc.BotSession, error)
	UpdateSessionTitle(context.Context, sqlc.UpdateSessionTitleParams) (sqlc.BotSession, error)
	UpdateSessionTypeAndMetadata(context.Context, sqlc.UpdateSessionTypeAndMetadataParams) (sqlc.BotSession, error)
}

Queries is the storage surface owned by the Thread domain. Route lookup and route activation intentionally stay outside this contract.

type Service

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

Service manages bot chat threads.

func NewService

func NewService(log *slog.Logger, queries Queries, publisher event.Publisher) *Service

NewService creates a thread service. publisher may be nil — thread creation still succeeds when there is no event hub wired in (tests, or any caller that doesn't surface activity events).

func (*Service) Create

func (s *Service) Create(ctx context.Context, input CreateInput) (Thread, error)

Create creates a new thread.

func (*Service) CreateSubagent

func (s *Service) CreateSubagent(ctx context.Context, input CreateSubagentInput) (Thread, SubagentConfig, error)

CreateSubagent atomically creates the hidden child thread and its pinned model/fork configuration when the backing store supports transactions.

func (*Service) ForkFromAssistantTurn

func (s *Service) ForkFromAssistantTurn(ctx context.Context, input ForkFromAssistantInput) (Thread, error)

ForkFromAssistantTurn creates a new chat thread containing the source thread's visible linear history through the selected assistant turn.

func (*Service) Get

func (s *Service) Get(ctx context.Context, sessionID string) (Thread, error)

func (*Service) GetSubagentConfig

func (s *Service) GetSubagentConfig(ctx context.Context, sessionID string) (SubagentConfig, error)

func (*Service) LatestModelPreferenceSeed added in v0.20.0

func (s *Service) LatestModelPreferenceSeed(ctx context.Context, botID, userID string) (string, string, error)

Get returns a session by ID. LatestModelPreferenceSeed is the welcome composer seed (issue #879, spec §3.4): the bot's most recent native user-facing session with a persisted pair, scoped to the calling user. Empty pair + nil error = no seed; welcome falls back to the bot default.

func (*Service) ListByBot

func (s *Service) ListByBot(ctx context.Context, botID string) ([]Thread, error)

ListByBot returns all active sessions for a bot.

func (*Service) ListByBotAndCreatedByUser

func (s *Service) ListByBotAndCreatedByUser(ctx context.Context, botID, userID string) ([]Thread, error)

ListByBotAndCreatedByUser returns all active sessions for a bot created by a user.

func (*Service) ListByBotAndCreatedByUserPaged

func (s *Service) ListByBotAndCreatedByUserPaged(ctx context.Context, botID, userID string, types []string, cursor Cursor, limit int64) ([]Thread, error)

ListByBotAndCreatedByUserPaged is the paged variant scoped to a single user.

func (*Service) ListByBotAndCreatedByUserPagedWithFilter

func (s *Service) ListByBotAndCreatedByUserPagedWithFilter(ctx context.Context, botID, userID string, types []string, cursor Cursor, limit int64, filter ListFilter) ([]Thread, error)

func (*Service) ListByBotPaged

func (s *Service) ListByBotPaged(ctx context.Context, botID string, types []string, cursor Cursor, limit int64) ([]Thread, error)

ListByBotPaged returns one page of sessions for a bot, filtered to the given types and starting after the given cursor. Callers that want a "has more" signal pass limit+1 and look for an extra row.

func (*Service) ListByBotPagedWithFilter

func (s *Service) ListByBotPagedWithFilter(ctx context.Context, botID string, types []string, cursor Cursor, limit int64, filter ListFilter) ([]Thread, error)

func (*Service) ListByRoute

func (s *Service) ListByRoute(ctx context.Context, routeID string) ([]Thread, error)

ListByRoute returns all active sessions for a route.

func (*Service) ListSubagentForkContext

func (s *Service) ListSubagentForkContext(ctx context.Context, sessionID string) ([]SubagentForkContextMessage, error)

ListSubagentForkContext loads only the hidden rows reserved for a forked subagent's inherited model context. Other invisible history rows are ignored.

func (*Service) ListSubagentsByParent

func (s *Service) ListSubagentsByParent(ctx context.Context, parentSessionID string) ([]Thread, error)

ListSubagentsByParent returns active subagent sessions created under a parent session.

func (*Service) MergeRuntimeMetadata added in v0.20.0

func (s *Service) MergeRuntimeMetadata(ctx context.Context, sessionID, runtimeType string, delta map[string]any) (Thread, error)

MergeRuntimeMetadata merges driver-owned keys into a session's runtime metadata; a nil value deletes the key. The write is guarded on runtimeType so a concurrent runtime switch turns the merge into ErrRuntimeMetadataStale instead of polluting another runtime's metadata, and on the caller's runtime fencing token so a superseded owner's late write after a cluster ownership handoff cannot clobber the new owner's runtime thread id.

func (*Service) MessageCount

func (s *Service) MessageCount(ctx context.Context, sessionID string) (int64, error)

func (*Service) SetACPSetupValidator

func (s *Service) SetACPSetupValidator(validator ACPSetupValidator)

func (*Service) SetHookService

func (s *Service) SetHookService(h *hooks.Service)

func (*Service) SoftDelete

func (s *Service) SoftDelete(ctx context.Context, sessionID string) error

SoftDelete marks a session as deleted.

func (*Service) Touch

func (s *Service) Touch(ctx context.Context, sessionID string) error

Touch updates a session's updated_at timestamp.

func (*Service) UpdateDescriptorAndMetadataWithOwner

func (s *Service) UpdateDescriptorAndMetadataWithOwner(ctx context.Context, sessionID, typ, sessionMode, runtimeType string, metadata, runtimeMetadata map[string]any, botAgentID *string, runtimeOwnerUserID string) (Thread, error)

UpdateDescriptorAndMetadataWithOwner updates the session mode/runtime descriptor directly. Callers that only patch metadata for a Phase 3 session must pass the existing descriptor so discuss+ACP sessions keep their runtime.

func (*Service) UpdateEmptyDescriptorAndMetadataWithOwner

func (s *Service) UpdateEmptyDescriptorAndMetadataWithOwner(ctx context.Context, sessionID, typ, sessionMode, runtimeType string, metadata, runtimeMetadata map[string]any, botAgentID *string, runtimeOwnerUserID string) (Thread, error)

UpdateEmptyDescriptorAndMetadataWithOwner updates a session runtime identity only while its visible history is still empty. PostgreSQL-backed stores lock the bot and session in the same transaction as the message check, descriptor update, runtime-fence bump, and ACP snapshot invalidation. The lock ordering matches runtime activation, so an already-running turn linearizes before the empty-history check and an old runtime token cannot write after the update.

func (*Service) UpdateMetadata

func (s *Service) UpdateMetadata(ctx context.Context, sessionID string, metadata map[string]any) (Thread, error)

UpdateMetadata updates a session's metadata.

func (*Service) UpdateTitle

func (s *Service) UpdateTitle(ctx context.Context, sessionID, title string) (Thread, error)

UpdateTitle updates a session's title.

func (*Service) UpdateTypeAndMetadata

func (s *Service) UpdateTypeAndMetadata(ctx context.Context, sessionID, typ string, metadata map[string]any) (Thread, error)

UpdateTypeAndMetadata updates a session's runtime type and metadata in one statement so callers don't expose a half-updated agent selection.

func (*Service) UpdateTypeAndMetadataWithOwner

func (s *Service) UpdateTypeAndMetadataWithOwner(ctx context.Context, sessionID, typ string, metadata map[string]any, runtimeOwnerUserID string) (Thread, error)

UpdateTypeAndMetadataWithOwner updates a session descriptor and binds any External Agent runtime ownership to a server-confirmed account id. The metadata owner field is never trusted from callers.

type SubagentConfig

type SubagentConfig struct {
	ThreadID     string    `json:"session_id"`
	ModelUUID    string    `json:"model_uuid,omitempty"`
	ModelID      string    `json:"model_id"`
	ProviderName string    `json:"provider"`
	Forked       bool      `json:"fork"`
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
}

SubagentConfig is the persisted runtime selection for a managed subagent.

type SubagentForkContextMessage

type SubagentForkContextMessage struct {
	SourceMessageID string          `json:"source_message_id,omitempty"`
	Role            string          `json:"role"`
	Message         json.RawMessage `json:"message"`
}

SubagentForkContextMessage is one hidden history message copied or materialized into a forked subagent session.

type Thread

type Thread struct {
	ID              string         `json:"id"`
	BotID           string         `json:"bot_id"`
	BotAgentID      string         `json:"bot_agent_id,omitempty"`
	RouteID         string         `json:"route_id,omitempty"`
	ChannelType     string         `json:"channel_type,omitempty"`
	Type            string         `json:"type"`
	SessionMode     string         `json:"session_mode"`
	RuntimeType     string         `json:"runtime_type"`
	RuntimeMetadata map[string]any `json:"runtime_metadata,omitempty"`
	Title           string         `json:"title"`
	Metadata        map[string]any `json:"metadata,omitempty"`
	ParentThreadID  string         `json:"parent_session_id,omitempty"`
	CreatedByUserID string         `json:"created_by_user_id,omitempty"`
	WorkdirID       string         `json:"workdir_id,omitempty"`
	CreatedAt       time.Time      `json:"created_at"`
	UpdatedAt       time.Time      `json:"updated_at"`
	// Preferred* is the session's persisted (model, effort) pair (issue #879).
	// Empty means "no memory"; the composer reseeds from it on open/repoint.
	PreferredExternalModelID string         `json:"preferred_external_model_id,omitempty"`
	ModelPreferenceRevision  string         `json:"model_preference_revision,omitempty"`
	PreferredChatModelID     string         `json:"preferred_chat_model_id,omitempty"`
	PreferredReasoningEffort string         `json:"preferred_reasoning_effort,omitempty"`
	RouteMetadata            map[string]any `json:"route_metadata,omitempty"`
	RouteConversationType    string         `json:"route_conversation_type,omitempty"`
	Visibility               Visibility     `json:"-"`

} // @name session.Session

Thread represents a chat thread within a bot.

type Visibility

type Visibility string

Visibility describes whether a thread belongs in user-facing history or is reserved for an internal agent workflow.

const (
	VisibilityUser     Visibility = "user"
	VisibilityInternal Visibility = "internal"
)

func NormalizeVisibility

func NormalizeVisibility(stored string, mode string) Visibility

NormalizeVisibility resolves a possibly-absent stored visibility value against the session mode's default. Exported for restore paths that replay archived session rows which may predate the visibility column.

Jump to

Keyboard shortcuts

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