Documentation
¶
Overview ¶
Package storage is the persistence layer for Glyphoxa: goose-driven schema migrations (ADR-0031) plus a thin pgx query layer over the core tables.
Query layer choice: pgx/v5 directly, not sqlc. The read surface this task needs is small (load an Agent + its Persona/Voice + bound Provider Configs), so hand-written queries are clearer than adding a codegen step to CI. goose needs a database/sql handle (see migrate.go); the app uses a *pgxpool.Pool.
Index ¶
- Constants
- Variables
- func BuildTSQuery(q string) string
- func EnsureCurrent(ctx context.Context, db *sql.DB) error
- func GrantsFromRows(rows []ToolGrant) []tool.Grant
- func MigrateDown(ctx context.Context, db *sql.DB) error
- func MigrateUp(ctx context.Context, db *sql.DB) error
- func NewMigrationProvider(db *sql.DB) (*goose.Provider, error)
- func ValidateEdge(t KGEdgeType, from, to KGNodeType) error
- func Version(ctx context.Context, db *sql.DB) (int64, error)
- func VoiceFromJSON(raw json.RawMessage) (tts.Voice, error)
- func VoiceToJSON(v tts.Voice) (json.RawMessage, error)
- type Agent
- type AgentLastSpoke
- type AgentRole
- type AgentUpdate
- type AppearanceHit
- type Campaign
- type CampaignMap
- type CampaignMapUpdate
- type CampaignUpdate
- type Character
- type CharacterUpdate
- type ChunkMatch
- type Component
- type DeploymentConfig
- type ExportChunk
- type GrantSurface
- type Highlight
- type HighlightEnrichTarget
- type HighlightSoundEnrichTarget
- type IndexableLine
- type IndexableNode
- type Job
- type JobStatus
- type KGBoard
- type KGEdge
- type KGEdgeType
- type KGEdgeWithNodes
- type KGGraphNode
- type KGNode
- type KGNodeAspect
- type KGNodeAspectWrite
- type KGNodePair
- type KGNodeType
- type KGNodeUpdate
- type KnowledgeDraftEdge
- type KnowledgeProposal
- type LoadedAgent
- type MapPin
- type MapPinUpdate
- type MigrationStatus
- type NewAgent
- type NewCampaign
- type NewCampaignMap
- type NewCharacter
- type NewKGEdge
- type NewKGNode
- type NewKGNodeAspect
- type NewMapPin
- type NewProviderConfig
- type NewSession
- type NewToolGrant
- type NodeAppearance
- type PartyMarker
- type Plan
- type PlanSpec
- type PlanSyncResult
- type PlanningMessage
- type PlanningThread
- type PromptKGView
- type ProposalBlockedError
- type ProviderConfig
- type QueryMetrics
- type QueryTracer
- type Session
- type SessionAppearance
- type SessionPurgeCandidate
- type SignupParams
- type SignupResult
- type SpendCaps
- type Store
- func (s *Store) AcquireOrRenewPresenceOwner(ctx context.Context, instanceID string, expiry time.Duration) (bool, error)
- func (s *Store) ActiveSubscription(ctx context.Context, tenantID uuid.UUID) (Subscription, error)
- func (s *Store) AddUsage(ctx context.Context, usageRows []UsageRow) error
- func (s *Store) AgentLinkedNode(ctx context.Context, agentID uuid.UUID) (KGNode, bool, error)
- func (s *Store) AgentNodeFacts(ctx context.Context, agentID uuid.UUID) ([]KGNode, error)
- func (s *Store) AnyLiveVoiceSessionIntent(ctx context.Context) (bool, error)
- func (s *Store) AppendPlanningMessage(ctx context.Context, campaignID, threadID uuid.UUID, role, content string) (PlanningMessage, error)
- func (s *Store) ApplyKnowledgeDraft(ctx context.Context, campaignID uuid.UUID, nodes []NewKGNode, ...) ([]KGNode, []KGEdge, error)
- func (s *Store) ApproveKnowledgeProposal(ctx context.Context, campaignID, id uuid.UUID) error
- func (s *Store) ArchiveCampaign(ctx context.Context, tenantID, id uuid.UUID) (Campaign, error)
- func (s *Store) AuthenticateSession(ctx context.Context, token string) (User, error)
- func (s *Store) BillingReport(ctx context.Context, from, to time.Time) ([]TenantBillingLine, error)
- func (s *Store) CampaignNodeNames(ctx context.Context, campaignID uuid.UUID) ([]IndexableNode, error)
- func (s *Store) CampaignTags(ctx context.Context, campaignID uuid.UUID) ([]TaggedNode, error)
- func (s *Store) CancelPendingVoiceSessionControl(ctx context.Context, id uuid.UUID) (bool, error)
- func (s *Store) CharacterAgents(ctx context.Context, campaignID uuid.UUID) ([]Agent, error)
- func (s *Store) ClaimJob(ctx context.Context, kinds []string, lease time.Duration) (Job, error)
- func (s *Store) ClaimVoiceSessionIntent(ctx context.Context, instanceID string) (VoiceSessionIntent, error)
- func (s *Store) CloseVoiceSession(ctx context.Context, id uuid.UUID, status VoiceSessionStatus, lineCount int, ...) (VoiceSession, error)
- func (s *Store) CompleteJob(ctx context.Context, id uuid.UUID, attempts int) error
- func (s *Store) CountJobBacklog(ctx context.Context, kinds []string) (map[string]int, error)
- func (s *Store) CountTranscriptLines(ctx context.Context, sessionID uuid.UUID) (int, error)
- func (s *Store) CountUnembeddedChunks(ctx context.Context) (int, error)
- func (s *Store) CountUnembeddedNodes(ctx context.Context) (int, error)
- func (s *Store) CountUnembeddedNodesInCampaign(ctx context.Context, campaignID uuid.UUID) (int, error)
- func (s *Store) CreateAgent(ctx context.Context, a NewAgent) (uuid.UUID, error)
- func (s *Store) CreateAgentWithNPCNode(ctx context.Context, a NewAgent) (uuid.UUID, error)
- func (s *Store) CreateBoard(ctx context.Context, campaignID uuid.UUID, name string) (KGBoard, error)
- func (s *Store) CreateCampaign(ctx context.Context, c NewCampaign) (uuid.UUID, error)
- func (s *Store) CreateCharacter(ctx context.Context, n NewCharacter) (uuid.UUID, error)
- func (s *Store) CreateEdge(ctx context.Context, e NewKGEdge) (KGEdge, error)
- func (s *Store) CreateHighlight(ctx context.Context, h Highlight) error
- func (s *Store) CreateKnowledgeProposal(ctx context.Context, campaignID, agentID uuid.UUID, proposedWrite []byte) error
- func (s *Store) CreateMap(ctx context.Context, m NewCampaignMap) (CampaignMap, error)
- func (s *Store) CreateNode(ctx context.Context, n NewKGNode) (KGNode, error)
- func (s *Store) CreateNodeWithAspects(ctx context.Context, n NewKGNode, aspects []NewKGNodeAspect) (KGNode, error)
- func (s *Store) CreatePin(ctx context.Context, n NewMapPin) (MapPin, error)
- func (s *Store) CreatePlanningThread(ctx context.Context, campaignID uuid.UUID, title string) (PlanningThread, error)
- func (s *Store) CreateProviderConfig(ctx context.Context, p NewProviderConfig) (uuid.UUID, error)
- func (s *Store) CreateSession(ctx context.Context, n NewSession) (Session, error)
- func (s *Store) CreateTenant(ctx context.Context, name string) (uuid.UUID, error)
- func (s *Store) CreateToolGrant(ctx context.Context, g NewToolGrant) (uuid.UUID, error)
- func (s *Store) CreateVoiceSession(ctx context.Context, campaignID uuid.UUID) (VoiceSession, error)
- func (s *Store) CreateVoiceSessionControl(ctx context.Context, c VoiceSessionControl) (VoiceSessionControl, error)
- func (s *Store) CreateVoiceSessionIntent(ctx context.Context, tenantID, campaignID uuid.UUID, voiceChannelID string) (VoiceSessionIntent, error)
- func (s *Store) DeleteAgent(ctx context.Context, campaignID, id uuid.UUID) error
- func (s *Store) DeleteBoard(ctx context.Context, campaignID, id uuid.UUID) error
- func (s *Store) DeleteCampaign(ctx context.Context, tenantID, id uuid.UUID) error
- func (s *Store) DeleteCampaignWithJob(ctx context.Context, tenantID, id uuid.UUID, jobKind string, jobPayload []byte) error
- func (s *Store) DeleteCharacter(ctx context.Context, campaignID, id uuid.UUID) error
- func (s *Store) DeleteEdge(ctx context.Context, campaignID, id uuid.UUID) error
- func (s *Store) DeleteHighlight(ctx context.Context, tenantID, id uuid.UUID) (string, error)
- func (s *Store) DeleteMap(ctx context.Context, campaignID, id uuid.UUID) (blobKey string, err error)
- func (s *Store) DeleteNode(ctx context.Context, campaignID, id uuid.UUID) (portraitBlobKey string, err error)
- func (s *Store) DeletePin(ctx context.Context, campaignID, id uuid.UUID) error
- func (s *Store) DeletePlanningThread(ctx context.Context, campaignID, id uuid.UUID) error
- func (s *Store) DeleteSession(ctx context.Context, token string) error
- func (s *Store) DeleteSessionCandidates(ctx context.Context, voiceSessionID uuid.UUID) (int, error)
- func (s *Store) DeleteTapeConsent(ctx context.Context, campaignID uuid.UUID, discordUserID string) error
- func (s *Store) DeleteToolGrant(ctx context.Context, agentID uuid.UUID, toolName string) error
- func (s *Store) DeleteTranscriptLine(ctx context.Context, sessionID uuid.UUID, lineID string) error
- func (s *Store) EndTenantPlan(ctx context.Context, tenantID uuid.UUID) error
- func (s *Store) EndVoiceSession(ctx context.Context, id uuid.UUID, lineCount int) (VoiceSession, error)
- func (s *Store) EnqueueJob(ctx context.Context, kind string, payload []byte, maxAttempts int) (uuid.UUID, error)
- func (s *Store) EnqueueJobAt(ctx context.Context, kind string, payload []byte, maxAttempts int, ...) (uuid.UUID, error)
- func (s *Store) FindCampaignByName(ctx context.Context, tenantID uuid.UUID, name string) (Campaign, error)
- func (s *Store) FindTenantByName(ctx context.Context, name string) (Tenant, error)
- func (s *Store) FinishVoiceSessionControl(ctx context.Context, id uuid.UUID, status VoiceSessionControlStatus, ...) (VoiceSessionControl, error)
- func (s *Store) FinishVoiceSessionIntent(ctx context.Context, id uuid.UUID, instanceID string, ...) (VoiceSessionIntent, error)
- func (s *Store) FirstLineIDAtOrAfter(ctx context.Context, voiceSessionID uuid.UUID, at time.Time) (string, error)
- func (s *Store) FirstTenant(ctx context.Context) (Tenant, error)
- func (s *Store) GetActiveCampaign(ctx context.Context) (Campaign, error)
- func (s *Store) GetActiveCampaignForUserInTenant(ctx context.Context, tenantID uuid.UUID, discordUserID string) (Campaign, error)
- func (s *Store) GetActiveCampaignInTenant(ctx context.Context, tenantID uuid.UUID) (Campaign, error)
- func (s *Store) GetAdmissionPosture(ctx context.Context) (string, error)
- func (s *Store) GetAgent(ctx context.Context, id uuid.UUID) (Agent, error)
- func (s *Store) GetButler(ctx context.Context, campaignID uuid.UUID) (Agent, error)
- func (s *Store) GetCampaign(ctx context.Context, id uuid.UUID) (Campaign, error)
- func (s *Store) GetCampaignInTenant(ctx context.Context, tenantID, id uuid.UUID) (Campaign, error)
- func (s *Store) GetCampaignShareChannel(ctx context.Context, campaignID uuid.UUID) (string, error)
- func (s *Store) GetCharacterByDiscordUser(ctx context.Context, campaignID uuid.UUID, discordUserID string) (Character, error)
- func (s *Store) GetDeploymentConfig(ctx context.Context, tenantID uuid.UUID) (DeploymentConfig, error)
- func (s *Store) GetEmbeddingsProviderConfig(ctx context.Context) (ProviderConfig, error)
- func (s *Store) GetHighlight(ctx context.Context, tenantID, id uuid.UUID) (Highlight, error)
- func (s *Store) GetJob(ctx context.Context, id uuid.UUID) (Job, error)
- func (s *Store) GetLatestVoiceSession(ctx context.Context, campaignID uuid.UUID) (VoiceSession, error)
- func (s *Store) GetLiveVoiceSessionIntentForTenant(ctx context.Context, tenantID uuid.UUID) (VoiceSessionIntent, error)
- func (s *Store) GetMap(ctx context.Context, campaignID, id uuid.UUID) (CampaignMap, error)
- func (s *Store) GetOperatorActiveCampaign(ctx context.Context) (Campaign, error)
- func (s *Store) GetPartyMarker(ctx context.Context, campaignID, sessionID uuid.UUID) (PartyMarker, error)
- func (s *Store) GetPendingKnowledgeProposal(ctx context.Context, campaignID, id uuid.UUID) (KnowledgeProposal, error)
- func (s *Store) GetPin(ctx context.Context, campaignID, id uuid.UUID) (MapPin, error)
- func (s *Store) GetPlanBySlug(ctx context.Context, slug string) (Plan, error)
- func (s *Store) GetPlanningThread(ctx context.Context, campaignID, id uuid.UUID) (PlanningThread, error)
- func (s *Store) GetProviderConfig(ctx context.Context, id uuid.UUID) (ProviderConfig, error)
- func (s *Store) GetProviderConfigByComponent(ctx context.Context, tenantID uuid.UUID, component Component) (ProviderConfig, error)
- func (s *Store) GetTenant(ctx context.Context, id uuid.UUID) (Tenant, error)
- func (s *Store) GetTenantIDByGuildID(ctx context.Context, guildID string) (uuid.UUID, error)
- func (s *Store) GetTenantSpendCaps(ctx context.Context, tenantID uuid.UUID) (SpendCaps, error)
- func (s *Store) GetUserByDiscordID(ctx context.Context, discordUserID string) (User, error)
- func (s *Store) GetVoiceSession(ctx context.Context, id uuid.UUID) (VoiceSession, error)
- func (s *Store) GetVoiceSessionControl(ctx context.Context, id uuid.UUID) (VoiceSessionControl, error)
- func (s *Store) GetVoiceSessionIntent(ctx context.Context, id uuid.UUID) (VoiceSessionIntent, error)
- func (s *Store) HasPresenceOwner(ctx context.Context) (bool, error)
- func (s *Store) HeartbeatVoiceSessionIntent(ctx context.Context, id uuid.UUID, instanceID string) (stopRequested bool, err error)
- func (s *Store) HighlightsExist(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]bool, error)
- func (s *Store) ImportVoiceSession(ctx context.Context, v VoiceSession) (uuid.UUID, error)
- func (s *Store) InTx(ctx context.Context, fn func(*Store) error) error
- func (s *Store) InsertTranscriptChunk(ctx context.Context, c TranscriptChunk) (uuid.UUID, error)
- func (s *Store) IsCampaignLiveIntent(ctx context.Context, campaignID uuid.UUID) (bool, error)
- func (s *Store) LastSpokenByAgent(ctx context.Context, campaignID uuid.UUID) ([]AgentLastSpoke, error)
- func (s *Store) ListAgents(ctx context.Context, campaignID uuid.UUID) ([]Agent, error)
- func (s *Store) ListAllCampaigns(ctx context.Context) ([]Campaign, error)
- func (s *Store) ListAllCampaignsInTenant(ctx context.Context, tenantID uuid.UUID) ([]Campaign, error)
- func (s *Store) ListBoards(ctx context.Context, campaignID uuid.UUID) ([]KGBoard, error)
- func (s *Store) ListCampaignHighlightClipKeys(ctx context.Context, campaignID uuid.UUID) ([]string, error)
- func (s *Store) ListCampaignMapBlobKeys(ctx context.Context, campaignID uuid.UUID) ([]string, error)
- func (s *Store) ListCampaignPortraitKeys(ctx context.Context, campaignID uuid.UUID) ([]string, error)
- func (s *Store) ListCampaigns(ctx context.Context) ([]Campaign, error)
- func (s *Store) ListCampaignsInTenant(ctx context.Context, tenantID uuid.UUID) ([]Campaign, error)
- func (s *Store) ListCharacters(ctx context.Context, campaignID uuid.UUID) ([]Character, error)
- func (s *Store) ListDeploymentConfigs(ctx context.Context) ([]DeploymentConfig, error)
- func (s *Store) ListEdges(ctx context.Context, campaignID uuid.UUID) ([]KGEdge, error)
- func (s *Store) ListGraphNodes(ctx context.Context, campaignID uuid.UUID) ([]KGGraphNode, error)
- func (s *Store) ListHighlights(ctx context.Context, tenantID, voiceSessionID uuid.UUID) ([]Highlight, error)
- func (s *Store) ListMaps(ctx context.Context, campaignID uuid.UUID) ([]CampaignMap, error)
- func (s *Store) ListNodeAppearances(ctx context.Context, campaignID, nodeID uuid.UUID, limit int) ([]AppearanceHit, error)
- func (s *Store) ListNodeAspects(ctx context.Context, campaignID, nodeID uuid.UUID) ([]KGNodeAspect, error)
- func (s *Store) ListNodes(ctx context.Context, campaignID uuid.UUID) ([]KGNode, error)
- func (s *Store) ListPendingKnowledgeProposals(ctx context.Context, campaignID uuid.UUID) ([]KnowledgeProposal, error)
- func (s *Store) ListPendingVoiceSessionControls(ctx context.Context, intentID uuid.UUID) ([]VoiceSessionControl, error)
- func (s *Store) ListPins(ctx context.Context, campaignID, mapID uuid.UUID) ([]MapPin, error)
- func (s *Store) ListPlanningMessages(ctx context.Context, campaignID, threadID uuid.UUID) ([]PlanningMessage, error)
- func (s *Store) ListPlanningThreads(ctx context.Context, campaignID uuid.UUID) ([]PlanningThread, error)
- func (s *Store) ListPlans(ctx context.Context) ([]Plan, error)
- func (s *Store) ListPlayerMaps(ctx context.Context, campaignID uuid.UUID) ([]CampaignMap, error)
- func (s *Store) ListPlayerPins(ctx context.Context, campaignID, mapID uuid.UUID) ([]MapPin, error)
- func (s *Store) ListPromotedHighlightsNeedingEnrichment(ctx context.Context, enrichKind string) ([]HighlightEnrichTarget, error)
- func (s *Store) ListPromotedHighlightsNeedingSoundEnrichment(ctx context.Context, enrichKind string) ([]HighlightSoundEnrichTarget, error)
- func (s *Store) ListProviderConfigs(ctx context.Context, tenantID uuid.UUID) ([]ProviderConfig, error)
- func (s *Store) ListSessionAppearances(ctx context.Context, sessionID uuid.UUID) ([]SessionAppearance, error)
- func (s *Store) ListSessionCandidateClipKeys(ctx context.Context, voiceSessionID uuid.UUID) ([]string, error)
- func (s *Store) ListSessionsNeedingCandidatePurge(ctx context.Context, purgeKind string) ([]SessionPurgeCandidate, error)
- func (s *Store) ListTapeConsent(ctx context.Context, campaignID uuid.UUID) ([]string, error)
- func (s *Store) ListTenantOperatorBindings(ctx context.Context) ([]TenantOperatorBinding, error)
- func (s *Store) ListTenantOperatorDiscordIDs(ctx context.Context) ([]string, error)
- func (s *Store) ListTenantsWithPlan(ctx context.Context) ([]TenantPlanRow, error)
- func (s *Store) ListToolGrants(ctx context.Context, agentID uuid.UUID) ([]ToolGrant, error)
- func (s *Store) ListToolGrantsFor(ctx context.Context, agentID uuid.UUID, surface GrantSurface) ([]ToolGrant, error)
- func (s *Store) ListTranscriptChunks(ctx context.Context, campaignID uuid.UUID, includeVectors bool) ([]ExportChunk, error)
- func (s *Store) ListTranscriptLines(ctx context.Context, sessionID uuid.UUID) ([]TranscriptLine, error)
- func (s *Store) ListUnembeddedChunks(ctx context.Context, limit int) ([]TranscriptChunk, error)
- func (s *Store) ListUnembeddedNodes(ctx context.Context, limit int) ([]KGNode, error)
- func (s *Store) ListVoiceSessions(ctx context.Context, campaignID uuid.UUID, limit int) ([]VoiceSession, error)
- func (s *Store) LoadAgent(ctx context.Context, id uuid.UUID) (LoadedAgent, error)
- func (s *Store) MapAncestors(ctx context.Context, campaignID, id uuid.UUID) ([]CampaignMap, error)
- func (s *Store) MapSeedContext(ctx context.Context, campaignID, nodeID uuid.UUID) (KGNode, []string, error)
- func (s *Store) MarkJobDead(ctx context.Context, id uuid.UUID, attempts int, lastError string) error
- func (s *Store) MarkVoiceSessionIntentLive(ctx context.Context, id uuid.UUID, instanceID string, voiceSessionID uuid.UUID) (VoiceSessionIntent, error)
- func (s *Store) NodeEdges(ctx context.Context, campaignID, nodeID uuid.UUID) (outgoing, incoming []KGEdgeWithNodes, err error)
- func (s *Store) NodePins(ctx context.Context, campaignID, nodeID uuid.UUID) ([]MapPin, error)
- func (s *Store) NodePortrait(ctx context.Context, campaignID, nodeID uuid.UUID) (string, time.Time, error)
- func (s *Store) NodeTags(ctx context.Context, campaignID, nodeID uuid.UUID) ([]string, error)
- func (s *Store) PinsNear(ctx context.Context, campaignID, mapID uuid.UUID, x, y, radius float64, ...) ([]MapPin, error)
- func (s *Store) PlayerNodePins(ctx context.Context, campaignID, nodeID uuid.UUID) ([]MapPin, error)
- func (s *Store) PortraitSeedContext(ctx context.Context, campaignID, nodeID uuid.UUID) (KGNode, error)
- func (s *Store) PromoteHighlight(ctx context.Context, tenantID, id uuid.UUID) (Highlight, error)
- func (s *Store) PromptKG() PromptKGView
- func (s *Store) ProvisionSignup(ctx context.Context, p SignupParams) (SignupResult, error)
- func (s *Store) ReapDeadVoiceSessionIntents(ctx context.Context, expiry time.Duration) (int64, error)
- func (s *Store) ReapVoiceSessionIntentIfExpired(ctx context.Context, id uuid.UUID, expiry time.Duration) (bool, error)
- func (s *Store) ReconcileOrphanedVoiceSessions(ctx context.Context) (int64, error)
- func (s *Store) ReconcileWorkerOrphanedVoiceSessions(ctx context.Context) (int64, error)
- func (s *Store) RecordAdmissionPosture(ctx context.Context, mode string) error
- func (s *Store) RecordNodeAppearances(ctx context.Context, rows []NodeAppearance) error
- func (s *Store) RejectKnowledgeProposal(ctx context.Context, campaignID, id uuid.UUID) error
- func (s *Store) ReleaseDiscordGuild(ctx context.Context, tenantID uuid.UUID, guildID string) (DeploymentConfig, error)
- func (s *Store) ReleaseHighlightEnrichClaim(ctx context.Context, id uuid.UUID) error
- func (s *Store) ReleaseHighlightSoundEnrichClaim(ctx context.Context, id uuid.UUID) error
- func (s *Store) ReleasePresenceOwner(ctx context.Context, instanceID string) error
- func (s *Store) RenameAgentNode(ctx context.Context, campaignID, agentID uuid.UUID, oldName, newName string) error
- func (s *Store) RenameBoard(ctx context.Context, campaignID, id uuid.UUID, name string) error
- func (s *Store) RenamePlanningThread(ctx context.Context, campaignID, id uuid.UUID, title string) (PlanningThread, error)
- func (s *Store) RenameTag(ctx context.Context, campaignID uuid.UUID, from, to string) error
- func (s *Store) RenameTenant(ctx context.Context, id uuid.UUID, name string) (Tenant, error)
- func (s *Store) ReplaceNodeAspects(ctx context.Context, campaignID, nodeID uuid.UUID, w KGNodeAspectWrite) error
- func (s *Store) RequestVoiceSessionStop(ctx context.Context, id uuid.UUID) (VoiceSessionIntent, error)
- func (s *Store) ResolveMapByName(ctx context.Context, campaignID uuid.UUID, name string) (CampaignMap, error)
- func (s *Store) ResolveOperatorTenant(ctx context.Context, userID uuid.UUID) (Tenant, error)
- func (s *Store) ResolvePinByLabel(ctx context.Context, campaignID, mapID uuid.UUID, label string) (MapPin, error)
- func (s *Store) RetryJob(ctx context.Context, id uuid.UUID, attempts int, lastError string, ...) error
- func (s *Store) RevokeSessionsOutsideAllowlist(ctx context.Context, discordUserIDs []string) (int64, error)
- func (s *Store) SaveDefaultVoiceChannel(ctx context.Context, tenantID uuid.UUID, voiceChannelID string) (DeploymentConfig, error)
- func (s *Store) SaveDiscordBotToken(ctx context.Context, tenantID uuid.UUID, ciphertext []byte, last4 string) (DeploymentConfig, error)
- func (s *Store) SaveDiscordChannels(ctx context.Context, tenantID uuid.UUID, guildID, voiceChannelID string) (DeploymentConfig, error)
- func (s *Store) SaveDiscordGuild(ctx context.Context, tenantID uuid.UUID, guildID string) (DeploymentConfig, error)
- func (s *Store) SearchChunksByAgent(ctx context.Context, campaignID, agentID uuid.UUID, query []float32, k int) ([]ChunkMatch, error)
- func (s *Store) SearchChunksByCampaign(ctx context.Context, campaignID uuid.UUID, query []float32, k int) ([]ChunkMatch, error)
- func (s *Store) SearchNodes(ctx context.Context, campaignID uuid.UUID, query string, limit int) ([]KGNode, error)
- func (s *Store) SearchPromotedHighlights(ctx context.Context, tenantID, campaignID uuid.UUID, query string, limit int) ([]Highlight, error)
- func (s *Store) SearchPublicNodes(ctx context.Context, campaignID uuid.UUID, query string, limit int) ([]KGNode, error)
- func (s *Store) SearchTranscriptLines(ctx context.Context, campaignID uuid.UUID, query string, limit int) ([]TranscriptLine, error)
- func (s *Store) SessionLinesForIndexing(ctx context.Context, sessionID uuid.UUID) ([]IndexableLine, error)
- func (s *Store) SetActiveCampaign(ctx context.Context, discordUserID string, campaignID uuid.UUID) error
- func (s *Store) SetBoardNodes(ctx context.Context, campaignID, boardID uuid.UUID, nodeIDs []uuid.UUID) error
- func (s *Store) SetCampaignShareChannel(ctx context.Context, campaignID uuid.UUID, channelID string) error
- func (s *Store) SetChunkEmbedding(ctx context.Context, id uuid.UUID, vec []float32, model string) error
- func (s *Store) SetHighlightImage(ctx context.Context, id uuid.UUID, imageKey, contentType string, ...) error
- func (s *Store) SetHighlightSound(ctx context.Context, id uuid.UUID, kind, soundKey, contentType string, ...) error
- func (s *Store) SetHighlightSoundKind(ctx context.Context, tenantID, id uuid.UUID, kind string) (Highlight, error)
- func (s *Store) SetMapImage(ctx context.Context, campaignID, id uuid.UUID, blobKey string, ...) (CampaignMap, error)
- func (s *Store) SetNodeAgent(ctx context.Context, campaignID, nodeID uuid.UUID, agentID uuid.NullUUID) (KGNode, error)
- func (s *Store) SetNodeEmbedding(ctx context.Context, id uuid.UUID, vec []float32, model string, ...) error
- func (s *Store) SetNodePortrait(ctx context.Context, campaignID, nodeID uuid.UUID, blobKey string) (KGNode, string, error)
- func (s *Store) SetNodeTags(ctx context.Context, campaignID, nodeID uuid.UUID, tags []string) error
- func (s *Store) SetPartyMarker(ctx context.Context, campaignID, sessionID uuid.UUID, ...) error
- func (s *Store) SetTenantPlan(ctx context.Context, tenantID uuid.UUID, slug string) (Subscription, error)
- func (s *Store) SetTenantSpendCaps(ctx context.Context, tenantID uuid.UUID, caps SpendCaps) error
- func (s *Store) SetUserSuspended(ctx context.Context, discordUserID string, suspended bool) error
- func (s *Store) SimilarNodePairs(ctx context.Context, campaignID uuid.UUID, minSimilarity float64, limit int) ([]KGNodePair, error)
- func (s *Store) SimilarNodes(ctx context.Context, campaignID uuid.UUID, query []float32, k int) ([]KGNode, error)
- func (s *Store) StartVoiceSessionControl(ctx context.Context, id uuid.UUID) (VoiceSessionControl, error)
- func (s *Store) SweepExpiredJobs(ctx context.Context, kinds []string) (int, error)
- func (s *Store) SweepOrphanedVoiceSessionControls(ctx context.Context, executingStale time.Duration) (int64, error)
- func (s *Store) SyncPlans(ctx context.Context, specs []PlanSpec, archiveMissing bool) (PlanSyncResult, error)
- func (s *Store) TenantForUser(ctx context.Context, userID uuid.UUID) (uuid.UUID, error)
- func (s *Store) TenantHasPlatformKeySource(ctx context.Context, tenantID uuid.UUID) (bool, error)
- func (s *Store) TenantIncludedUsageUSD(ctx context.Context, tenantID uuid.UUID) (*float64, error)
- func (s *Store) TenantMonthUsageUSD(ctx context.Context, tenantID uuid.UUID, from, to time.Time) (float64, error)
- func (s *Store) TryClaimHighlightEnrich(ctx context.Context, id uuid.UUID, ttl time.Duration) (bool, error)
- func (s *Store) TryClaimHighlightSoundEnrich(ctx context.Context, id uuid.UUID, ttl time.Duration) (bool, error)
- func (s *Store) UnarchiveCampaign(ctx context.Context, tenantID, id uuid.UUID) (Campaign, error)
- func (s *Store) UnpinnedNodes(ctx context.Context, campaignID, mapID uuid.UUID) ([]KGNode, error)
- func (s *Store) UpdateAgent(ctx context.Context, a AgentUpdate) (Agent, error)
- func (s *Store) UpdateBoard(ctx context.Context, campaignID, id uuid.UUID, name string, ...) error
- func (s *Store) UpdateCampaign(ctx context.Context, c CampaignUpdate) (Campaign, error)
- func (s *Store) UpdateCharacter(ctx context.Context, u CharacterUpdate) (Character, error)
- func (s *Store) UpdateEdgeDetails(ctx context.Context, campaignID, id uuid.UUID, note string, disposition int) (KGEdge, error)
- func (s *Store) UpdateMap(ctx context.Context, u CampaignMapUpdate) (CampaignMap, error)
- func (s *Store) UpdateNode(ctx context.Context, u KGNodeUpdate) (KGNode, error)
- func (s *Store) UpdateNodeWithAspects(ctx context.Context, u KGNodeUpdate, w KGNodeAspectWrite) (KGNode, error)
- func (s *Store) UpdatePin(ctx context.Context, u MapPinUpdate) (MapPin, error)
- func (s *Store) UpsertProviderConfigs(ctx context.Context, configs []NewProviderConfig) ([]ProviderConfig, error)
- func (s *Store) UpsertTapeConsent(ctx context.Context, campaignID uuid.UUID, discordUserID string) error
- func (s *Store) UpsertToolGrant(ctx context.Context, g NewToolGrant) error
- func (s *Store) UpsertTranscriptLine(ctx context.Context, l TranscriptLine) error
- func (s *Store) UpsertUser(ctx context.Context, p UpsertUserParams) (User, error)
- func (s *Store) VoiceSessionInTenant(ctx context.Context, tenantID, sessionID uuid.UUID) (bool, error)
- type Subscription
- type TaggedNode
- type Tenant
- type TenantBillingLine
- type TenantOperatorBinding
- type TenantPlanRow
- type ToolGrant
- type TranscriptChunk
- type TranscriptLine
- type UpsertUserParams
- type UsageRow
- type User
- type VoiceSession
- type VoiceSessionControl
- type VoiceSessionControlKind
- type VoiceSessionControlStatus
- type VoiceSessionIntent
- type VoiceSessionIntentStatus
- type VoiceSessionStatus
Constants ¶
const ( // HighlightCandidate: freshly detected, subject to the 7-day purge. HighlightCandidate = "candidate" // HighlightPromoted: an explicit GM keep; never purged. HighlightPromoted = "promoted" )
Highlight status values (CHECK-constrained in the schema).
const ( // SoundKindSting: a short sound-effect sting (the ElevenLabs // sound-effects endpoint, bounded to ≤22s). SoundKindSting = "sting" // SoundKindMusic: a composed Music track (the ElevenLabs Music endpoint). SoundKindMusic = "music" )
Highlight sound kinds (CHECK-constrained in the schema, #312): the GM's standing "Add sound" choice on a promoted Highlight. Empty means none requested (the default).
const ( PlanningRoleUser = "user" PlanningRoleAssistant = "assistant" )
The two planning message roles. The CHECK constraint in 00053 mirrors them.
const DefaultJobMaxAttempts = 5
DefaultJobMaxAttempts is the retry budget applied when a job is enqueued with a non-positive maxAttempts. After this many attempts a failing job goes to JobDead rather than retrying forever (ADR-0049 dead-letter policy).
const DevOperatorDiscordID = "glyphoxa-dev-operator"
DevOperatorDiscordID is the synthetic Discord identity the GLYPHOXA_DEV_MODE boot upserts as the dev operator (ADR-0041). It is deliberately NOT a real snowflake so it can never collide with a genuine Discord user. It lives here because ResolveOperatorTenant treats a tenant bound to it as still claimable: the first REAL operator login takes the tenant (and everything configured in dev mode) over, instead of being stranded next to it in a fresh empty tenant.
const MaxAgentFactNodes = kgFactsCap
MaxAgentFactNodes is [kgFactsCap], exported so the GM-facing preview (#535) can tell the GM that the SQL read itself clipped their NPC's neighbourhood. Without it, a hub NPC with more than this many neighbours would show the extras as "not adjacent" — indistinguishable from a missing Edge, and unfixable because the GM would be looking for the wrong problem.
const MaxAppearances = 50
MaxAppearances caps one entry's Appearances list. A long-running campaign mentions its central NPC in hundreds of lines, and "the last N times" is the question; an unbounded list is a slow read nobody scrolls.
const MaxBoardNameRunes = 120
MaxBoardNameRunes bounds a board name. Tags were capped and board names were not, so a board could carry a megabyte of text that every ListBoards response then had to return.
const MaxTagRunes = 40
MaxTagRunes bounds one tag. A tag is a label, not a sentence.
const MaxTagsPerNode = 20
MaxTagsPerNode bounds how many an entry may carry — enough for real organization, few enough that the chip row stays readable.
const VoiceSessionReasonOrphaned = "orphaned: reconciled at startup"
VoiceSessionReasonOrphaned is the end_reason stamped by the boot-time reconciliation (#143): the row was still 'running' but no live loop owned it (crash / kill -9 / a failed end-write), so startup closed it. A NULL end_reason means the session ended through the normal Stop / loop-exit path.
Variables ¶
var ErrAspectsFull = errors.New("storage: node has reached its aspect limit")
ErrAspectsFull is returned by the approve path when a Node already carries kgvocab.MaxAspectsPerNode Aspects. It is a refusal, not a failure: silently exceeding the cap would leave the entry permanently unsaveable in the editor, which validates against the same cap — the GM would be unable to fix even a typo without first deleting rows they never chose to add.
var ErrButlerUndeletable = errors.New("storage: butler cannot be deleted")
ErrButlerUndeletable is returned by DeleteAgent when the target Agent is a Butler. The Butler is an invariant of a Campaign's existence (ADR-0009): it is auto-created and cannot be removed. The RPC layer maps this to Connect CodeFailedPrecondition.
var ErrConflict = errors.New("storage: conflict")
ErrConflict is returned when a write hits a UNIQUE constraint (Postgres 23505): a duplicate (from, to, type) Edge, or an Agent already linked to another Node. The RPC layer maps it to CodeAlreadyExists.
var ErrGuildTaken = errors.New("storage: this guild is already bound by another tenant")
ErrGuildTaken is returned by SaveDiscordChannels when the guild_id is already bound by a DIFFERENT Tenant — the first-registrar-wins unique index (#483; full guild-permission proof is #504). The RPC layer maps it to CodeFailedPrecondition so the save is refused instead of silently rebinding another Tenant's guild (the old newest-wins read let the rebinder read the victim's voice-channel members and hijack its command routing).
var ErrIntentActive = errors.New("storage: a voice session intent is already active for this tenant")
ErrIntentActive is returned by CreateVoiceSessionIntent when the Tenant already has a non-terminal (pending/claimed/live) intent — the one-live-per-tenant partial UNIQUE index (23505). The RPC layer maps it to CodeAlreadyExists, mirroring session.ErrSessionActive.
var ErrInvalidDisposition = errors.New("storage: disposition must be between -2 and +2")
ErrInvalidDisposition is returned when a disposition falls outside -2..+2.
var ErrInvalidEdge = errors.New("storage: invalid edge")
ErrInvalidEdge is returned when an Edge's (type, from-type, to-type) combination violates the amendment's validity matrix, or when an Agent link targets a non-NPC Node (the DB CHECK). The RPC layer maps it to CodeInvalidArgument.
var ErrInvalidMapParent = errors.New("storage: a map cannot contain itself")
ErrInvalidMapParent is returned when a re-parent would make a Map its own ancestor.
var ErrInvalidPin = errors.New("storage: pin coordinates must be within 0..1")
ErrInvalidPin is returned when a Pin's coordinates fall outside the normalized 0..1 range the schema enforces.
var ErrInvalidTag = errors.New("storage: invalid tag")
ErrInvalidTag is returned for a blank or over-long tag.
var ErrNotArchived = errors.New("storage: campaign not archived")
ErrNotArchived is returned by DeleteCampaign when the target campaign exists but is not archived: the hard delete is refused until the campaign has been archived first (#269, decided on #265). The RPC layer maps it to Connect CodeFailedPrecondition.
var ErrNotFound = errors.New("storage: not found")
ErrNotFound is returned when a query matches no row.
var ErrNoteTooLong = errors.New("storage: edge note is too long")
ErrNoteTooLong is returned when an Edge note exceeds kgvocab.MaxEdgeNoteRunes.
var ErrPlanArchived = errors.New("storage: plan is archived")
ErrPlanArchived is returned by SetTenantPlan for an archived plan slug: an archived tier accepts no NEW subscriptions (existing ones keep running).
var ErrUserSuspended = errors.New("storage: user is suspended")
ErrUserSuspended reports a signup/login attempt by a suspended user (users.suspended_at set, ADR-0055 open-mode revocation). The OAuth callback bounces these at the door instead of minting a session the per-request re-check would refuse anyway.
Functions ¶
func BuildTSQuery ¶
BuildTSQuery turns a raw GM query string into a safe to_tsquery('simple') input. tsquery operator characters (& | ! ( ) : * …) are never passed through as operators: every non-letter/non-digit rune is a term separator, so a malicious or accidental operator can only ever split words. Surviving terms AND-join, and only the LAST term gets the ":*" prefix marker (typeahead — the word the GM is still typing). Returns "" when nothing survives, which SearchNodes treats as a no-op (no matches, not an error).
func EnsureCurrent ¶
EnsureCurrent verifies the DB schema is at the latest known version. web and voice Modes call this at startup and fail fast if the schema is behind (ADR-0031) — they never auto-migrate.
func GrantsFromRows ¶
GrantsFromRows maps an Agent's persisted Tool Grant rows to the in-memory [tool.Grant]s the live loop hydrates into a tool.GrantSet (#113, ADR-0029) — the identical shape the orchestrator consumes, so the loop never knows the grants came from the DB. A row's jsonb config becomes Grant.Config as a json.RawMessage (nil when the column is NULL — dice's shape); the Tool handler receives it as grantConfig at execution time and enforces scope there, never the LLM. No rows yields no grants: the Agent is shown no Tool at all.
This is the SINGLE canonical row→Grant mapping. wirenpc's live loop and the grant RPC's AC4 hydration test both call it, so neither can drift from the other (issue #215). It is the one place in this package that depends on pkg/tool — the vendor-neutral CRUD in tool_grant.go keeps the storage rows a raw blob; only this shared mapper crosses into the Tool domain, and the dependency is one-way (pkg/tool never imports internal/*).
func MigrateDown ¶
MigrateDown rolls back the most recently applied migration (locked).
func MigrateUp ¶
MigrateUp applies all pending migrations (locked). Called at startup in `all` Mode (ADR-0031); web/voice Modes do not auto-migrate.
func NewMigrationProvider ¶
NewMigrationProvider builds the goose Provider over the embedded migrations, configured with the Postgres session locker required by ADR-0031.
db must be a *sql.DB on a Postgres driver (e.g. pgx/v5/stdlib). goose needs a database/sql handle; the application's own queries use a pgxpool separately.
func ValidateEdge ¶
func ValidateEdge(t KGEdgeType, from, to KGNodeType) error
ValidateEdge enforces the ADR-0008 amendment's object-side-only validity matrix. Structural edge types constrain their target (resides_in → Location, member_of → Faction, participated_in → PlotThread); parent_of constrains both ends to Character/NPC. The subject side of the structural types and every social/loose type (knows, owns, enemy_of, ally_of, mentioned_in) accept any Node type — the domain legitimately contains sentient swords that know kings. It is pure (no DB): the create path validates before the INSERT.
func VoiceFromJSON ¶
func VoiceFromJSON(raw json.RawMessage) (tts.Voice, error)
VoiceFromJSON decodes an Agent's persisted voice JSONB into a tts.Voice. It is the SINGLE canonical reader for the voice column, mirroring GrantsFromRows (#215): the voice pipeline's hydration and the Campaign RPC's editor mapping both go through it, so a writer and a reader can never drift into the silent NPC of issue #224.
The CANONICAL SHAPE is the Go-default field names of tts.Voice ({"ProviderID","VoiceID","Name","Language","Settings"}) — the shape the seed rows already hold and the pipeline already reads. tts.Voice deliberately carries NO json tags (ADR-0022 keeps Settings opaque and the type untouched); tagging it would orphan every healthy row.
An empty column or a bare {} — the schema default and the editor's "no voice" — decodes to the zero Voice with a nil error. A Settings persisted as the JSON literal null normalizes to a nil Settings so a settings-less Voice round-trips identically. A genuinely unparsable blob is an error, never a silent zero.
func VoiceToJSON ¶
func VoiceToJSON(v tts.Voice) (json.RawMessage, error)
VoiceToJSON encodes a tts.Voice into the canonical voice JSONB VoiceFromJSON reads back — the write counterpart, so both directions share one shape.
Types ¶
type Agent ¶
type Agent struct {
ID uuid.UUID
CampaignID uuid.UUID
Role AgentRole
Name string
// Title is the Agent's role subtitle shown in the editor (e.g. "Gruff
// innkeeper"); free text, may be empty.
Title string
// Persona: markdown personality/backstory/speech style.
Persona string
// Voice (ADR-0022/0023): TTS provider + voice-id config, stored as JSONB.
Voice json.RawMessage
// VoiceProviderConfigID is the TTS Provider Config backing this Agent's Voice.
VoiceProviderConfigID uuid.NullUUID
// LLMProviderConfigID is the LLM Provider Config this Agent reasons with.
// May be null; resolving a tenant default when null is a #6 concern (the
// schema has no is_default marker yet, so no fallback is wired here).
LLMProviderConfigID uuid.NullUUID
// ChatLLMProviderConfigID is the Butler's chat-scoped LLM slot (ADR-0062):
// the planning chat resolves it FIRST, so upgrading chat quality never
// touches the voice loop's LLMProviderConfigID. May be null (the ladder
// falls back to the tenant 'chat_llm' then 'llm' Provider Config). Written
// by no editor surface in v1 — UpdateAgent deliberately leaves the column
// alone, mirroring how it preserves fields the editor never sees.
ChatLLMProviderConfigID uuid.NullUUID
// AddressOnly: reachable only by explicit name/alias (ADR-0024). Butler true.
AddressOnly bool
// SpeakerColor is a server-assigned palette SLOT (not a colour value): the web
// tier maps it onto its speaker palette so each roster member renders in a
// stable hue across reloads (#71). Assigned round-robin per Campaign on
// Character insert; the Butler keeps slot 0.
SpeakerColor int
Aliases []string
CreatedAt time.Time
UpdatedAt time.Time
}
Agent is an AI-controlled persona — Butler or Character NPC (ADR-0009).
type AgentLastSpoke ¶ added in v0.5.0
AgentLastSpoke is one Agent's most recent committed Transcript Line (#544). Who is the Line's speaker label, which for an Agent turn is its display NAME — transcript_line carries no agent_id (ADR-0040 persists what was said and by whom-as-shown, not a foreign key), so the roster matches on name. A renamed Agent therefore loses its history here; that is a display nicety on a prep dashboard, not a correctness claim, and inventing a join would mean changing what the Line grain records.
type AgentRole ¶
type AgentRole string
AgentRole is an Agent's archetype (ADR-0009): the agents table is polymorphic over this enum so one orchestrator/address-detection path serves both.
type AgentUpdate ¶
type AgentUpdate struct {
ID uuid.UUID
CampaignID uuid.UUID
Name string
Title string
Persona string
Voice []byte // JSON; defaults to {} when nil
VoiceProviderConfigID uuid.NullUUID
LLMProviderConfigID uuid.NullUUID
AddressOnly bool
Aliases []string
}
AgentUpdate is the input to UpdateAgent. It carries the editor-editable fields only — the Campaign screen edits name/title/persona/voice/address-only/aliases; agent_role and speaker_color are immutable here. CampaignID is the owning Campaign the write is scoped to (#342): the UPDATE matches (id, campaign_id), so an Agent in another Campaign is invisible and yields ErrNotFound — cross-campaign mutation is refused, and an Agent never moves between Campaigns. A Butler's address_only is force-kept true regardless of AddressOnly (ADR-0009 / ADR-0024).
type AppearanceHit ¶ added in v0.5.0
type AppearanceHit struct {
VoiceSessionID uuid.UUID
LineID string
At time.Time
Who string
Kind string
Text string
// SessionStartedAt lets the UI label which session a mention came from without
// a second read.
SessionStartedAt time.Time
}
AppearanceHit is one appearance joined to the line that produced it — what the entry editor's Appearances list renders and deep-links from.
type Campaign ¶
type Campaign struct {
ID uuid.UUID
TenantID uuid.UUID
// GMMemberID references a Member (Member Role 'gm'). The members table is
// task #6, so this is a bare nullable UUID for now (SEAM #6).
GMMemberID uuid.NullUUID
Name string
System string
Language string
CreatedAt time.Time
UpdatedAt time.Time
// ArchivedAt is when the campaign was archived (#269), or nil when active.
// Archived campaigns are excluded from ListCampaigns, the /glyphoxa use
// autocomplete, and the GetActiveCampaign most-recent fallback, and cannot back
// a Voice Session.
ArchivedAt *time.Time
// TapeArmed is the GM opt-in that arms the rollover tape for this Campaign's
// Voice Sessions (#306, ADR-0051; default false, capture hard-disabled without
// it). Appended LAST in campaignColumns/scanCampaign (column-order coupling), so
// any new column follows it in both places.
TapeArmed bool
}
Campaign is a persistent TTRPG game owned by a Tenant and GM'd by one Member.
type CampaignMap ¶ added in v0.5.0
type CampaignMap struct {
ID uuid.UUID
CampaignID uuid.UUID
Name string
BlobKey string
WidthPx int
HeightPx int
// ParentMapID is the Map this one sits inside; AnchorNodeID is the Location
// Node this Map DEPICTS. Together they give the continent → region → city →
// building hierarchy GMs actually draw, and let a pin on one map open the map
// beneath it.
ParentMapID uuid.NullUUID
AnchorNodeID uuid.NullUUID
GMPrivate bool
CreatedAt time.Time
UpdatedAt time.Time
}
CampaignMap is one persisted Map. BlobKey addresses the image in the blob seam; WidthPx/HeightPx are the source dimensions, kept for aspect-ratio rendering and deliberately NOT used for coordinate maths.
type CampaignMapUpdate ¶ added in v0.5.0
type CampaignMapUpdate struct {
ID uuid.UUID
CampaignID uuid.UUID
Name string
ParentMapID uuid.NullUUID
AnchorNodeID uuid.NullUUID
GMPrivate bool
}
CampaignMapUpdate is the Map editor's field set. blob_key and dimensions are updated separately by the re-upload path, which is what keeps a rescale from touching pins.
type CampaignUpdate ¶
type CampaignUpdate struct {
// TenantID is the owning Tenant the write is scoped to (#473): the UPDATE matches
// (id, tenant_id), so a campaign in another Tenant is invisible and yields
// ErrNotFound — a cross-tenant rename is refused, never a permission error that
// would confirm the id exists. The RPC handler fills it from auth.TenantID(ctx).
TenantID uuid.UUID
ID uuid.UUID
Name string
System string
Language string
// TapeArmed, when non-nil, sets the rollover-tape opt-in (#306, ADR-0051); nil
// leaves the current value unchanged (the proto field is `optional`, so an
// UpdateCampaign that does not touch the tape must not silently disarm it).
TapeArmed *bool
}
CampaignUpdate is the input to UpdateCampaign. It carries the operator-editable fields only — name/system/language; tenant_id, gm_member_id and the timestamps are not settable here. System/Language are written verbatim as opaque free-text strings (no validation at this layer), mirroring how they are stored on create.
type Character ¶
type Character struct {
ID uuid.UUID
CampaignID uuid.UUID
Name string
Aliases []string
DiscordUserID string
// LinkedUserID is nil until the Player first signs in via Discord OAuth
// (ADR-0003); it never becomes NULL-mandatory like discord_user_id.
LinkedUserID *string
CreatedAt time.Time
UpdatedAt time.Time
}
Character is one persisted Player Character in a Campaign.
type CharacterUpdate ¶
type CharacterUpdate struct {
ID uuid.UUID
CampaignID uuid.UUID
Name string
Aliases []string
DiscordUserID string
}
CharacterUpdate is the input to UpdateCharacter — a full-field save of the editor fields. DiscordUserID is included so an operator can rebind a Character to a different Discord User (it stays NOT NULL). CampaignID is the owning Campaign the write is scoped to (#342): the UPDATE matches (id, campaign_id), so a row in another Campaign is invisible and yields ErrNotFound — a Character never moves between Campaigns, and no operator can mutate one they do not own.
type ChunkMatch ¶
type ChunkMatch struct {
Chunk TranscriptChunk
Distance float64
}
ChunkMatch is one Transcript Chunk returned by ANN retrieval together with its cosine distance to the query vector (#119, ADR-0011). Distance comes from pgvector's <=> operator: smaller = nearer, ascending order = nearest first.
type Component ¶
type Component string
Component is a Provider category a Provider Config binds to (ADR-0004).
const ( ComponentLLM Component = "llm" ComponentSTT Component = "stt" ComponentTTS Component = "tts" ComponentEmbeddings Component = "embeddings" ComponentS2S Component = "s2s" // ComponentImage is AI image generation (#311, Epic 8, ADR-0004 amendment): // the enum value the 00028 migration adds. Gemini is its v1 provider. ComponentImage Component = "image" // ComponentChatLLM is the tenant-level chat-scoped LLM slot (#592, // ADR-0062): the Butler planning chat's resolution ladder reads it between // the Butler's own chat slot and the voice 'llm' config, so chat can run a // quality-over-latency model without re-pricing the live voice loop. The // 00053 migration adds the enum value. ComponentChatLLM Component = "chat_llm" )
type DeploymentConfig ¶
type DeploymentConfig struct {
TenantID uuid.UUID
DiscordBotTokenCiphertext []byte
DiscordBotTokenLast4 string
GuildID string
VoiceChannelID string
CreatedAt time.Time
UpdatedAt time.Time
}
DeploymentConfig is the single-operator Discord integration the Configuration screen edits (#68): the deployment Bot token — a write-only secret, sealed at rest like a Provider Config — plus the non-secret Guild / Voice channel IDs. The Bot is deployment-shared (one token regardless of Tenant, CONTEXT.md), so this is distinct from the per-Component, Tenant-scoped provider_config (ADR-0004); it is keyed by tenant_id only for the MVP single operator (ADR-0039). DiscordBotTokenCiphertext is empty until a token is saved.
type ExportChunk ¶
type ExportChunk struct {
TranscriptChunk
Embedding string
}
ExportChunk is one Transcript Chunk plus its embedding rendered as pgvector's text form ("[...]") for the Campaign Bundle exporter (#288, ADR-0053). Embedding is "" when the row's vector is NULL or when the caller excluded vectors — the default export strips embeddings (ADR-0053 d3), so the destination re-embeds.
type GrantSurface ¶ added in v0.8.0
type GrantSurface string
GrantSurface is the Agent surface a Tool Grant arms (#592, ADR-0062): the live voice loop and the Butler planning chat hydrate DISJOINT grant rows, so the two surfaces cannot leak tools into each other.
const ( // GrantSurfaceVoice arms the live voice loop — the pre-0062 meaning of // every grant row, and the column default. GrantSurfaceVoice GrantSurface = "voice" // GrantSurfaceChat arms the Butler planning chat's tool belt (ADR-0062). GrantSurfaceChat GrantSurface = "chat" )
type Highlight ¶
type Highlight struct {
ID uuid.UUID
TenantID uuid.UUID
VoiceSessionID uuid.UUID
CampaignID uuid.UUID
Status string
StartsAt time.Time
EndsAt time.Time
Score float64
Excerpt string
Reason string
SpeakerIDs []string
ClipKey string
ClipContentType string
ClipSizeBytes int64
// ImageKey / ImageContentType / ImageSizeBytes carry the AI-generated scene
// (#311, Epic 8, ADR-0004 amendment): the enrichment job stores an image
// behind the blob seam (ADR-0048) and lands it here. ImageKey == "" means no
// image yet (unenriched, unconfigured, or a failed generation — the row stays
// intact without media). ImageKey is NEVER exposed on the wire (clip_key
// posture): the image is served through GET /highlights/{id}/image.
ImageKey string
ImageContentType string
ImageSizeBytes int64
// SoundKind / SoundRequestedAt / SoundKey / SoundContentType /
// SoundSizeBytes carry the GM's opt-in sound enrichment (#312, Epic 8,
// ADR-0004 amendment): SoundKind is the standing choice ("sting" or
// "music", "" = none), SoundRequestedAt stamps the latest request (the web
// tier's await-media poll bound), and the key/content-type/size triad
// mirrors the image's. SoundKey == "" with SoundKind != "" means requested
// but not landed (generating, unconfigured, or failed — the row stays
// intact without media). SoundKey is NEVER exposed on the wire (clip_key
// posture): the audio is served through GET /highlights/{id}/sound.
SoundKind string
SoundRequestedAt *time.Time
SoundKey string
SoundContentType string
SoundSizeBytes int64
CreatedAt time.Time
PromotedAt *time.Time
}
Highlight is one persisted Session Highlight row (#308).
type HighlightEnrichTarget ¶ added in v0.2.1
HighlightEnrichTarget is a promoted, still-imageless Highlight the boot reconciliation sweep must (re)enqueue image enrichment for (#406): the id plus the tenant that owns it, exactly the enrich job payload's two fields (the sweep carries no ambient tenant — it is process-wide, ADR-0049).
type HighlightSoundEnrichTarget ¶ added in v0.10.0
HighlightSoundEnrichTarget is a promoted Highlight whose requested sound never landed and has no live generation job — the boot reconciliation sweep's re-enqueue input (#312, the #406 pattern). Kind rides along because the sound job payload carries the requested kind (unlike the image's).
type IndexableLine ¶ added in v0.5.0
IndexableLine is one committed Transcript Line the indexer scans.
type IndexableNode ¶ added in v0.5.0
IndexableNode is one Node the indexer matches against: its id and its name, and nothing else. Deliberately no prose — the indexer matches NAMES, and hauling every entry's body across for that would be pure waste.
type Job ¶
type Job struct {
ID uuid.UUID
Kind string
Payload []byte // jsonb; the handler-scoped payload (carries its own scope, no tenant_id column)
Status JobStatus
Attempts int
MaxAttempts int
RunAfter time.Time
LeasedUntil *time.Time
LastError *string
CreatedAt time.Time
UpdatedAt time.Time
}
Job is one row of the background job queue (#286, ADR-0049).
type JobStatus ¶
type JobStatus string
JobStatus is the lifecycle state of a job row (CHECK-constrained in the schema).
const ( // JobPending: not yet claimed; runnable once run_after <= now(). JobPending JobStatus = "pending" // JobRunning: claimed by a worker, leased_until in the future. JobRunning JobStatus = "running" // JobDone: handler succeeded; terminal. JobDone JobStatus = "done" // JobDead: exhausted its attempts (or swept); terminal, last_error kept. JobDead JobStatus = "dead" )
type KGBoard ¶ added in v0.5.0
type KGBoard struct {
ID uuid.UUID
CampaignID uuid.UUID
Name string
NodeIDs []uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
}
KGBoard is a named, ordered set of entries the GM pins for one session — the "tonight: the harbour heist" list they already keep in a text file outside the tool. NodeIDs is in board order.
type KGEdge ¶
type KGEdge struct {
ID uuid.UUID
CampaignID uuid.UUID
FromNodeID uuid.UUID
ToNodeID uuid.UUID
Type KGEdgeType
// Note is the relation's texture — "owes money to", "since the siege" (#546).
// A bare `knows` carries nothing; this is what makes it a relationship.
Note string
// Disposition is how the SUBJECT feels about the object, -2..+2 (hostile,
// cold, neutral, warm, devoted). Edges are strictly directional with no
// auto-inverse (ADR-0008 amendment), which is exactly what makes asymmetric
// feelings expressible: a secretly hostile ally is one edge with a negative
// disposition and no matching return edge.
Disposition int
CreatedAt time.Time
}
KGEdge is one persisted typed directional Edge between two Nodes in a Campaign.
func (KGEdge) DispositionClause ¶ added in v0.5.0
DispositionClause renders an Edge's feeling as ONE short clause for the fact block, or "" when there is nothing to say (#546).
Strictly one clause, because it spends the shared MaxBlockChars budget that world facts also draw on — and renderFacts stops at the first fact that would overrun, so a verbose relationship would evict knowledge outright.
type KGEdgeType ¶
type KGEdgeType string
KGEdgeType is a Knowledge Graph Edge's type (CONTEXT.md "Edge", ADR-0008). It mirrors the kg_edge_type Postgres enum. The values are compiler-linked to the single relation vocabulary in pkg/kgvocab (#449), which the remember_knowledge Tool's schema/validation also derives from.
const ( KGEdgeResidesIn KGEdgeType = kgvocab.RelationResidesIn KGEdgeMemberOf KGEdgeType = kgvocab.RelationMemberOf KGEdgeOwns KGEdgeType = kgvocab.RelationOwns KGEdgeKnows KGEdgeType = kgvocab.RelationKnows KGEdgeEnemyOf KGEdgeType = kgvocab.RelationEnemyOf KGEdgeAllyOf KGEdgeType = kgvocab.RelationAllyOf KGEdgeParentOf KGEdgeType = kgvocab.RelationParentOf KGEdgeParticipatedIn KGEdgeType = kgvocab.RelationParticipatedIn KGEdgeMentionedIn KGEdgeType = kgvocab.RelationMentionedIn )
type KGEdgeWithNodes ¶
type KGEdgeWithNodes struct {
KGEdge
FromName string
FromType KGNodeType
ToName string
ToType KGNodeType
}
KGEdgeWithNodes is an Edge joined to its two endpoints' display fields, so the Campaign screen renders an incident-edge list without an N+1 per endpoint.
type KGGraphNode ¶ added in v0.5.0
type KGGraphNode struct {
ID uuid.UUID
Type KGNodeType
Name string
GMPrivate bool
AgentID uuid.NullUUID
BodyLen int
AspectCount int
// PublicAspectCount counts only the Aspects a prompt can actually receive.
// The distinction matters: an entry whose ONLY fact is a GM secret is authored
// (so AspectCount is 1, and the GM's own map should say so) but says nothing to
// its NPC (so the health and readiness derivations must read 0, or they report
// exactly the state they exist to catch as healthy).
PublicAspectCount int
}
KGGraphNode is one Node as the graph view needs it: identity, type, name and the two flags the rendering distinguishes on — WITHOUT body or aspect text. BodyLen and AspectCount are the "does this entry actually say anything" signal the health panel and the readiness marks derive from; sending the text itself would multiply the payload for something no node glyph renders.
type KGNode ¶
type KGNode struct {
ID uuid.UUID
CampaignID uuid.UUID
Type KGNodeType
Name string
Body string
GMPrivate bool
// AgentID is the optional NPC-Node ↔ Character NPC Agent link (#132, ADR-0008
// amendment): the "voiced by" cast Agent. Only an NPC Node may carry it (DB
// CHECK); NULL when the Node is wiki-only or not an NPC.
AgentID uuid.NullUUID
CreatedAt time.Time
UpdatedAt time.Time
// PortraitBlobKey names the Node's portrait bytes in the blob seam (#590);
// empty means no portrait. Written only by SetNodePortrait — the editor
// mutations never touch it, so a save cannot drop a portrait.
PortraitBlobKey string
// Aspects is the Node's ordered per-fact visibility layer (#542, ADR-0008 third
// amendment), populated only by the reads that project it. A PROMPT-FACING read
// returns public Aspects ONLY — the exclusion lives in the read's SQL, so this
// field can never carry a GM secret into prompt assembly. nil means "this Node
// has none" for a projecting read, and "not loaded" for one that does not
// project them (the RETURNING projections of Create/Update).
Aspects []KGNodeAspect
// Relation is how the READING Agent relates to this Node, populated only by
// [Store.AgentNodeFacts] (#546): the disposition and note on the Agent's own
// OUTGOING edge to it. Outgoing only, because an Edge is a one-way assertion —
// how someone else feels about this Node is not this NPC's feeling.
RelationNote string
RelationDisposition int
}
KGNode is one persisted Knowledge Graph Node in a Campaign. GMPrivate hides the Node from any NPC's Hot Context (#126); Body is the Node's prose (empty by default).
func (KGNode) AspectLines ¶ added in v0.5.0
AspectLines renders a Node's Aspects as "Key: Value" lines — the flat form the embedding text consumes, where the key carries real signal about what kind of fact the value is. publicOnly drops gm_private rows.
A row with no value renders as its key alone; a fully empty row is skipped.
func (KGNode) AspectValues ¶ added in v0.5.0
AspectValues returns a Node's Aspect VALUES — without their keys — the granularity the ADR-0052 write-time dedup compares at, because a proposal's salient text is the fact itself and the key is only how it is filed (#411, #542). publicOnly is mandatory for every prompt-reachable consumer: a matched established fact is quoted back to the model in the Tool result, so a private Aspect matched here would leak a GM secret the prompt seam never carried.
type KGNodeAspect ¶ added in v0.5.0
type KGNodeAspect struct {
ID uuid.UUID `json:"id"`
Position int `json:"position"`
Key string `json:"key"`
Value string `json:"value"`
GMPrivate bool `json:"gm_private"`
}
KGNodeAspect is one persisted Aspect of a Node. Position is the author order within its Node (0-based, dense after every ReplaceNodeAspects). GMPrivate hides THIS row — and only this row — from every prompt-facing read.
The json tags are the wire format of the jsonb aggregate the Node reads pack their Aspects into (see kgNodeAspectsExpr), not a stored document: the columns are real columns.
type KGNodeAspectWrite ¶ added in v0.5.0
type KGNodeAspectWrite struct {
Known []uuid.UUID
Rows []NewKGNodeAspect
}
KGNodeAspectWrite is one editor save of a Node's Aspects (#542). Rows carries the list the GM authored, in their order, each keeping the id of the row it came from. Known carries the ids the editor had LOADED.
Replace-in-full is the right editor contract (one save covers add, edit, reorder and delete, with no client-side identity bookkeeping), but the naive form — "delete everything, insert the list" — has two failure modes, and the fix for one must not create the other:
- It silently destroys an Aspect that a Knowledge Proposal approval appended while the editor was open. Known bounds the delete to rows the editor actually saw, so a row it never knew about survives.
- Deleting and reinserting ROTATES every row's id, which makes a second save from a stale client (a second tab, or a failed background refetch) match nothing and DUPLICATE the entire list. So rows are updated in place by id and ids stay stable across saves; a save that repeats itself is idempotent.
type KGNodePair ¶ added in v0.5.0
KGNodePair is two Nodes in one Campaign whose embeddings are close — a PROBABLE-duplicate hint for the world health panel (#536). Similarity is 1 minus pgvector's cosine distance (<=>), so it ranges over [-1, 1]: 1.0 is identical direction, 0 orthogonal, negative opposed. Only the high end is ever surfaced, so the sign never reaches a caller in practice.
type KGNodeType ¶
type KGNodeType string
KGNodeType is a Knowledge Graph Node's type (CONTEXT.md "Node", ADR-0008). It mirrors the kg_node_type Postgres enum; the value is immutable after create. The values are compiler-linked to the single node-type vocabulary in pkg/kgvocab (#449), which the remember_knowledge Tool's schema/validation and the GM-facing label map also derive from.
const ( KGNodeCharacter KGNodeType = kgvocab.NodeTypeCharacter KGNodeNPC KGNodeType = kgvocab.NodeTypeNPC KGNodeLocation KGNodeType = kgvocab.NodeTypeLocation KGNodeFaction KGNodeType = kgvocab.NodeTypeFaction KGNodeItem KGNodeType = kgvocab.NodeTypeItem KGNodePlotThread KGNodeType = kgvocab.NodeTypePlotThread KGNodeNote KGNodeType = kgvocab.NodeTypeNote )
type KGNodeUpdate ¶
type KGNodeUpdate struct {
ID uuid.UUID
CampaignID uuid.UUID
Name string
Body string
GMPrivate bool
}
KGNodeUpdate is the input to UpdateNode — the Knowledge panel's editor fields (#129). It carries no Type (node_type is immutable, ADR-0008). CampaignID is the owning Campaign the write is scoped to (#342): the UPDATE matches (id, campaign_id), so a Node in another Campaign is invisible and yields ErrNotFound — a Node never moves between campaigns, and cross-campaign mutation is refused.
type KnowledgeDraftEdge ¶ added in v0.3.0
type KnowledgeDraftEdge struct {
FromIndex int
ToIndex int
Type KGEdgeType
}
KnowledgeDraftEdge is one edge of a knowledge draft to apply, referencing the draft's node list by index — the nodes have no ids until the apply lands.
type KnowledgeProposal ¶
type KnowledgeProposal struct {
ID uuid.UUID
CampaignID uuid.UUID
AuthoringAgentID uuid.UUID
AuthoringAgentName string
ProposedWrite json.RawMessage
Status string
CreatedAt time.Time
ReviewedAt *time.Time
}
KnowledgeProposal is one persisted proposal row. ProposedWrite is the raw jsonb tagged union; ReviewedAt is nil until the GM acts. AuthoringAgentName is the filing Agent's display name, joined from the agents table by the list/get reads (empty on a bare row that skipped the join).
type LoadedAgent ¶
type LoadedAgent struct {
Agent Agent
LLMConfig *ProviderConfig
TTSConfig *ProviderConfig
}
LoadedAgent is an Agent with its bound Provider Configs resolved — the bundle the orchestrator needs to bring an Agent to life (Persona, Voice, LLM/TTS configs). Either config may be nil if the Agent has none bound.
type MapPin ¶ added in v0.5.0
type MapPin struct {
ID uuid.UUID
MapID uuid.UUID
CampaignID uuid.UUID
NodeID uuid.UUID
X, Y float64
LabelOverride string
GMPrivate bool
NodeName string
NodeType KGNodeType
NodeGMPrivate bool
}
MapPin is one persisted Pin: a normalized position on a Map for a KG Node. NodeName and NodeType are joined from kg_node so the Maps tab can draw a pin without a second read; LabelOverride replaces the Node's name on this Map only ("the back door" for an entry called "Rusty Anchor cellar").
func (MapPin) Hidden ¶ added in v0.5.0
Hidden reports whether a Pin must be withheld from a player-tier view (ADR-0056). A Pin is hidden by its OWN flag or by its Node's: a position that points at a GM secret is itself a leak, which mirrors ADR-0008's rule that gm_private filtering applies to neighbour expansion and not only to direct reads.
type MapPinUpdate ¶ added in v0.5.0
type MapPinUpdate struct {
ID uuid.UUID
CampaignID uuid.UUID
X, Y float64
LabelOverride string
GMPrivate bool
}
MapPinUpdate moves or relabels a Pin. Position and presentation are one write because dragging a pin and renaming it are the same act to the GM: adjusting what this Map says about that entry.
type MigrationStatus ¶
MigrationStatus is one migration's applied/pending state, for `migrate status`.
type NewAgent ¶
type NewAgent struct {
CampaignID uuid.UUID
Role AgentRole
Name string
Title string
Persona string
Voice []byte // JSON; defaults to {} when nil
VoiceProviderConfigID uuid.NullUUID
LLMProviderConfigID uuid.NullUUID
AddressOnly bool
Aliases []string
}
NewAgent is the input to CreateAgent. Voice is the opaque JSONB blob the voice domain serializes its tts.Voice into; storage keeps it vendor-neutral. SpeakerColor is NOT an input — it is server-assigned on Character insert.
type NewCampaign ¶
NewCampaign is the input to CreateCampaign. GMMemberID is left zero (the members table is task #6); the column is nullable.
type NewCampaignMap ¶ added in v0.5.0
type NewCampaignMap struct {
CampaignID uuid.UUID
Name string
BlobKey string
WidthPx int
HeightPx int
ParentMapID uuid.NullUUID
AnchorNodeID uuid.NullUUID
GMPrivate bool
}
NewCampaignMap is the input to CreateMap. The blob is written FIRST and its key passed here, so a row never references bytes that do not exist.
type NewCharacter ¶
NewCharacter is the input to CreateCharacter. LinkedUserID is intentionally absent — a Character is created from its Discord identity and only gains a linked user later, via OAuth.
type NewKGEdge ¶
type NewKGEdge struct {
CampaignID uuid.UUID
FromNodeID uuid.UUID
ToNodeID uuid.UUID
Type KGEdgeType
}
NewKGEdge is the input to CreateEdge. The endpoints must be same-Campaign Nodes; the CampaignID scopes the endpoint lookup and pins both composite FKs.
type NewKGNode ¶
type NewKGNode struct {
CampaignID uuid.UUID
Type KGNodeType
Name string
Body string
GMPrivate bool
}
NewKGNode is the input to CreateNode. node_type is set once at insert and never updated (ADR-0008: type is immutable).
type NewKGNodeAspect ¶ added in v0.5.0
NewKGNodeAspect is one Aspect row as the editor supplies it. Position is NOT carried: Store.ReplaceNodeAspects assigns it from the slice order, so the author order is whatever the GM dragged the rows into and dense positions are an invariant rather than a client responsibility.
ID identifies an EXISTING row the caller is editing (uuid.Nil for a row the GM just added). Carrying it is what keeps a row's identity stable across saves: an unrecognised id is treated as a new row rather than trusted, so a stale client can add duplicates only by genuinely re-adding content, never by re-saving.
type NewMapPin ¶ added in v0.5.0
type NewMapPin struct {
MapID uuid.UUID
CampaignID uuid.UUID
NodeID uuid.UUID
X, Y float64
LabelOverride string
GMPrivate bool
}
NewMapPin is the input to CreatePin. Coordinates are normalized 0..1 and the DB CHECK enforces it, so an out-of-range pin is refused rather than stored and silently clamped at render time.
type NewProviderConfig ¶
type NewProviderConfig struct {
TenantID uuid.UUID
Component Component
Provider string
Model string
CredentialsCiphertext []byte
CredentialsLast4 string
}
NewProviderConfig is the input to CreateProviderConfig. CredentialsCiphertext is the AES-GCM-sealed credential (see internal/storage/crypto); for the self-host voice path the real key lives in the OS keyring and this carries a sealed placeholder with CredentialsLast4="env" (ADR-0004 / #5 seam).
type NewSession ¶
NewSession is the input to CreateSession. Token is the opaque random secret the auth tier minted; ExpiresAt is the absolute expiry the validator enforces.
type NewToolGrant ¶
type NewToolGrant struct {
AgentID uuid.UUID
ToolName string
Surface GrantSurface
Config json.RawMessage
}
NewToolGrant is the input to CreateToolGrant. Config is the optional per-grant scope blob (jsonb); a nil/empty Config persists SQL NULL — "no narrowing" (dice). Surface picks the grant's Agent surface (#592, ADR-0062); the zero value means GrantSurfaceVoice, the pre-0062 meaning of every caller.
type NodeAppearance ¶ added in v0.5.0
type NodeAppearance struct {
NodeID uuid.UUID
CampaignID uuid.UUID
VoiceSessionID uuid.UUID
LineID string
At time.Time
}
NodeAppearance is one recorded mention: a Node named in a committed Transcript Line.
type PartyMarker ¶ added in v0.5.0
type PartyMarker struct {
MapID uuid.NullUUID
PinID uuid.NullUUID
X, Y *float64
MapName string
// PinLabel and PinNodeID describe the Pin when the marker is at one.
PinLabel string
PinNodeID uuid.NullUUID
MapGMPrivate bool
PinHidden bool
}
PartyMarker is a Voice Session's current position. MapID zero means no marker is set — the state every session starts in.
A marker is either AT a Pin (PinID set) or at a free position between pins (X/Y set): the party crossing the moor is as real a position as the party in the tavern, and forcing a Pin for it would mean inventing wiki entries for empty ground.
func (PartyMarker) Set ¶ added in v0.5.0
func (m PartyMarker) Set() bool
Set reports whether a marker is placed at all.
type Plan ¶ added in v0.2.1
type Plan struct {
ID uuid.UUID
Slug string
DisplayName string
Description string
MonthlyPriceUSD float64
KeySource string
IncludedUsageUSD *float64
Limits json.RawMessage
Archived bool
CreatedAt time.Time
UpdatedAt time.Time
}
Plan is a catalog row.
type PlanSpec ¶ added in v0.2.1
type PlanSpec struct {
Slug string
DisplayName string
Description string
MonthlyPriceUSD float64
KeySource string // 'byok' | 'platform' (plan_key_source enum)
IncludedUsageUSD *float64
Limits json.RawMessage
}
PlanSpec is the write shape SyncPlans upserts from the operator's catalog file (ADR-0054). Limits is raw JSON (validated upstream); nil stores '{}'.
type PlanSyncResult ¶ added in v0.2.1
PlanSyncResult reports what one SyncPlans call changed.
type PlanningMessage ¶ added in v0.8.0
type PlanningMessage struct {
ID uuid.UUID
ThreadID uuid.UUID
CampaignID uuid.UUID
Seq int64
Role string
Content string
CreatedAt time.Time
}
PlanningMessage is one prose turn of a planning thread. Role is the two-value chat vocabulary ('user' / 'assistant'); Seq is the thread-local order the exchange loop appends under.
type PlanningThread ¶ added in v0.8.0
type PlanningThread struct {
ID uuid.UUID
CampaignID uuid.UUID
// Title is the thread's display name; auto-filled from the first user
// message when the GM did not name it.
Title string
CreatedAt time.Time
UpdatedAt time.Time
}
PlanningThread is one persisted Butler planning chat conversation.
type PromptKGView ¶ added in v0.2.1
type PromptKGView struct {
// contains filtered or unexported fields
}
PromptKGView is the prompt-facing Knowledge Graph read surface (#450, ADR-0008): the type prompt-assembly code (Hot Context fact recall, the kg_query Tool adapter) holds for KG reads. Every method filters gm_private rows inside its SQL, and the type exposes NO unfiltered read — "reaches a prompt" and "may see gm_private" are separated by the type system instead of by remembering the right *Store method name at each call site. GM and web review surfaces keep the full-KG reads on *Store (SearchNodes, SimilarNodes, ListNodes), which never flow into prompt assembly.
The zero-lag filtering semantics are those of the underlying *Store reads; the gm_private exclusion is pinned by the seam integration test (TestPromptKG_NeverReturnsGMPrivate).
func (PromptKGView) AgentNodeFacts ¶ added in v0.2.1
AgentNodeFacts is the Agent's own edge-aware Node neighbourhood, gm-public only (see Store.AgentNodeFacts).
func (PromptKGView) SearchPublicNodes ¶ added in v0.2.1
func (v PromptKGView) SearchPublicNodes(ctx context.Context, campaignID uuid.UUID, query string, limit int) ([]KGNode, error)
SearchPublicNodes is the prompt-facing KG search: gm_private Nodes are excluded in the query, before the LIMIT (see Store.SearchPublicNodes).
type ProposalBlockedError ¶ added in v0.2.1
type ProposalBlockedError struct{ Reason string }
ProposalBlockedError is returned by ApproveKnowledgeProposal when the proposed write cannot land as-is (an unresolvable/ambiguous subject, a dangling anchor, an edge matrix violation or duplicate, or an unreadable payload). Reason is a human-actionable message the GM sees; the proposal row stays pending so the GM can fix the wiki and re-approve, or reject. The RPC layer maps it to CodeFailedPrecondition with Reason verbatim.
func (*ProposalBlockedError) Error ¶ added in v0.2.1
func (e *ProposalBlockedError) Error() string
type ProviderConfig ¶
type ProviderConfig struct {
ID uuid.UUID
TenantID uuid.UUID
Component Component
Provider string
Model string
CredentialsCiphertext []byte
CredentialsLast4 string
CreatedAt time.Time
UpdatedAt time.Time
}
ProviderConfig is a Tenant-scoped, encrypted BYOK credential record binding a Component to a Provider (ADR-0004). Credentials are write-only after save; CredentialsCiphertext is AES-GCM, and only Last4 is plaintext for display.
type QueryMetrics ¶ added in v0.12.0
type QueryMetrics interface {
// DBQuery records one completed query under a BOUNDED family label.
DBQuery(query string, d time.Duration)
}
QueryMetrics is the storage-side seam for the query-latency histogram (#605). *observe.PrometheusRecorder satisfies it; storage depends on this local interface rather than on internal/observe, so the DB layer stays free of a metrics dependency and a test can substitute a sink.
type QueryTracer ¶ added in v0.12.0
type QueryTracer struct {
// contains filtered or unexported fields
}
QueryTracer implements pgx.QueryTracer, timing every query the pool runs and recording it under its bounded family label (#605). It is deliberately alloc-light on the hot path — the ANN search it measures runs inside the 250ms recall budget (ADR-0042), so End does a context read and one Observe: no logging, no map allocation, no SQL inspection.
SendBatch (pgx.BatchTracer) is out of scope: this tracer only implements QueryTracer, so batched statements are not timed.
func NewQueryTracer ¶ added in v0.12.0
func NewQueryTracer(rec QueryMetrics) *QueryTracer
NewQueryTracer returns a tracer recording into rec. Attach it to a pool via pgxpool.Config.ConnConfig.Tracer.
func (*QueryTracer) TraceQueryEnd ¶ added in v0.12.0
func (t *QueryTracer) TraceQueryEnd(ctx context.Context, _ *pgx.Conn, _ pgx.TraceQueryEndData)
TraceQueryEnd records the elapsed time under the ctx's query family.
func (*QueryTracer) TraceQueryStart ¶ added in v0.12.0
func (t *QueryTracer) TraceQueryStart(ctx context.Context, _ *pgx.Conn, _ pgx.TraceQueryStartData) context.Context
TraceQueryStart stashes the start instant on the returned context.
type Session ¶
type Session struct {
ID uuid.UUID
UserID uuid.UUID
Token string
CreatedAt time.Time
LastSeenAt time.Time
ExpiresAt time.Time
IP string
UA string
}
Session is a server-side login session (ADR-0016): the Token is the opaque random secret carried in the glyphoxa_session cookie, and this row is the authority. ExpiresAt gates validity; deleting the row revokes instantly.
type SessionAppearance ¶ added in v0.5.0
SessionAppearance is one session's appearance row, for the Campaign Bundle export (#547). It carries the Node id and the Line's own stable key — the two things an import has to remap or preserve.
type SessionPurgeCandidate ¶
SessionPurgeCandidate is an ended Voice Session the boot backstop must schedule a candidate purge for: its id plus the ended_at the 7-day horizon anchors to (ADR-0051), so a session that ended long ago is purged immediately rather than 7 days after boot.
type SignupParams ¶ added in v0.3.0
type SignupParams struct {
User UpsertUserParams
TenantName string
PlanSlug string
Session NewSession
// AUPAcceptedAt is when this signup acknowledged the Nutzungsbedingungen +
// Datenschutzerklärung (#518): the OAuth start carried the acknowledgment
// and the callback stamps its time here, so users.aup_accepted_at always
// holds the LATEST acceptance (a returning open-mode login re-acknowledges
// — the login screen requires the tick every time). The zero value writes
// nothing; the auth tier refuses ack-less open-mode signups before calling.
AUPAcceptedAt time.Time
}
SignupParams is the input to ProvisionSignup. Session.UserID is ignored — the provision fills it with the upserted user's id.
type SignupResult ¶ added in v0.3.0
SignupResult reports what ProvisionSignup did. Created is true when a fresh Tenant was founded (a first signup) — the callback uses it to route the user into onboarding rather than straight to the app.
type SpendCaps ¶
SpendCaps is the get/set DTO for a Tenant's two spend caps (#130, ADR-0046), each nil when that cap is unset. It is the storage-layer value the session Manager maps onto its meter and the RPC round-trips; keeping it distinct from spend.Caps keeps storage free of a spend-package import.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store reads and writes the core tables over a pgx connection pool (or a transaction, when created by Store.InTx).
func (*Store) AcquireOrRenewPresenceOwner ¶ added in v0.4.0
func (s *Store) AcquireOrRenewPresenceOwner(ctx context.Context, instanceID string, expiry time.Duration) (bool, error)
AcquireOrRenewPresenceOwner atomically claims or renews the singleton presence-owner row for instanceID and reports whether instanceID now owns it. The single upsert wins — inserts on an empty table, or updates the existing row — only when the row is already instanceID's (a renew, advancing the heartbeat) OR the incumbent's heartbeat is older than expiry (a dead owner's row, so a challenger takes over). When another live instance holds it the ON CONFLICT WHERE predicate is false, no row is written, and this returns false — the caller is a non-owner and must stay inactive.
func (*Store) ActiveSubscription ¶ added in v0.2.1
ActiveSubscription returns a Tenant's active subscription, ErrNotFound when none (an unsubscribed tenant — the BYOK self-host default).
func (*Store) AddUsage ¶ added in v0.2.1
AddUsage upsert-accumulates ledger rows: an existing (tenant, day, component, provider, model) bucket has the quantities and estimate ADDED, a new bucket is inserted. Idempotence is NOT promised — the flush path must not double-send — but ordering is irrelevant and rows commute, so concurrent sessions of one tenant simply accumulate.
func (*Store) AgentLinkedNode ¶
AgentLinkedNode returns the Node an Agent is linked to (the NPC-Node↔Agent link, ADR-0008; kg_node.agent_id), the anchor an own_node-scoped remember_knowledge proposal attaches to. ok=false (no error) means the Agent has no linked entry — the Tool handler refuses rather than proposing against a wrong Node. The link is unique per Agent, so at most one row exists.
func (*Store) AgentNodeFacts ¶
AgentNodeFacts returns the edge-aware Hot Context fact set for a Character NPC Agent (#133, ADR-0008 amendment): the Agent's own linked Node plus its edge-adjacent Nodes (a single hop in BOTH edge directions), gm-public only, newest-first within hop, capped — one round trip inside the kgfacts budget.
Semantics: traversal STARTS from the linked Node regardless of its gm_private, but gm_private filters SURFACING — the own Node and any neighbour that is gm_private is walked (its edges still expand) yet never returned, so a GM-only fact never reaches the prompt. An Agent with no linked Node yields an empty set (no campaign-wide fallback: the NPC injects only its own neighbourhood). The UNION dedupes multi-edge neighbours; min(hop) keeps the own Node at hop 0 even if an Edge also makes it a neighbour of itself's neighbour.
Aspects (#542) ride the same seam: each returned Node carries its PUBLIC Aspects only, filtered inside the aggregate's SQL. A gm_private Aspect on an otherwise public Node is therefore unreachable from prompt assembly by construction — which is the whole point of splitting visibility per fact.
func (*Store) AnyLiveVoiceSessionIntent ¶ added in v0.4.0
AnyLiveVoiceSessionIntent reports whether ANY non-terminal intent exists — the split-mode health signal (#491, the claim-plane sibling of Manager.AnyLive) the web tier reads for the Discord health short-circuit.
func (*Store) AppendPlanningMessage ¶ added in v0.8.0
func (s *Store) AppendPlanningMessage(ctx context.Context, campaignID, threadID uuid.UUID, role, content string) (PlanningMessage, error)
AppendPlanningMessage appends one prose turn at the thread's next seq and bumps the thread's updated_at (the thread list orders by it). The thread row is locked (FOR UPDATE) inside the transaction first, which both yields a clean ErrNotFound for an unknown/foreign thread — BEFORE the seq computation can trip the UNIQUE (thread_id, seq) index on a cross-campaign probe — and serializes concurrent appends to the same thread so max+1 never collides.
func (*Store) ApplyKnowledgeDraft ¶ added in v0.3.0
func (s *Store) ApplyKnowledgeDraft(ctx context.Context, campaignID uuid.UUID, nodes []NewKGNode, edges []KnowledgeDraftEdge) ([]KGNode, []KGEdge, error)
ApplyKnowledgeDraft creates a GM-confirmed knowledge draft's Nodes and Edges in ONE transaction (#479): either the whole draft lands or none of it. Every Node insert reuses CreateNode's semantics (enum-cast type); every Edge insert reuses createEdgeTx, so a matrix-invalid or self edge is ErrInvalidEdge and a duplicate (from, to, type) is ErrConflict — identical to a GM-authored CreateEdge — and any such failure rolls the WHOLE draft back (nothing half-lands). An out-of-range edge index is ErrInvalidEdge. The caller pre-validates indices and the matrix (the RPC layer refuses obviously bad drafts with better messages); this re-check is the transactional authority.
func (*Store) ApproveKnowledgeProposal ¶ added in v0.2.1
ApproveKnowledgeProposal lands a pending proposal's write on the Knowledge Graph and marks it approved — ATOMICALLY (#300, ADR-0052): the claim UPDATE and the KG write share one transaction, so a refused write rolls the claim back and the row stays pending (never a half-approved proposal, never a lost race). The claim is conditional on status='pending' and takes the row lock, so a concurrent double-approve sees 0 rows and yields ErrNotFound. An unreadable payload (wrong version / unknown kind) or an unlandable write (unresolvable/ambiguous subject, dangling anchor, edge matrix violation, duplicate, self-edge) yields a *ProposalBlockedError with a human reason and leaves the row pending.
Per ADR-0052 there is NO auto-merge: a fact/edge subject that names no wiki entry is refused with an actionable message, never silently created or fuzzily matched.
func (*Store) ArchiveCampaign ¶
ArchiveCampaign marks a campaign archived and returns the updated row (#269). It is idempotent: COALESCE(archived_at, now()) keeps an already-archived campaign's original timestamp (the audit trail of WHEN it was first archived), so a re-archive is a no-op on the timestamp. In the same transaction it clears users.active_campaign_id for every operator whose durable /glyphoxa use selection pointed at this campaign — the decided "archived durable selection is treated as absent" (#265): the slash surface then falls to its /use hint and the web tier to its most-recent fallback, neither of which resolves an archived campaign. A missing id yields ErrNotFound.
It is TENANT-SCOPED (#473): the UPDATE matches (id, tenant_id), so a foreign-tenant id is invisible and yields ErrNotFound — a cross-tenant archive can never land. The durable-selection clear stays keyed on the campaign id, so it only nulls pointers at THIS campaign.
func (*Store) AuthenticateSession ¶
AuthenticateSession validates a session token and returns the owning user. It bumps last_seen_at as a side effect of a successful, non-expired lookup, in a single round trip. A missing or expired token yields ErrNotFound — the RPC layer maps that to CodeUnauthenticated. A SUSPENDED owner yields the same ErrNotFound: this is the ADR-0055 per-request authorization re-check, and it lives in this query because every gated request on both transports funnels through here — suspension takes effect on the very next request with zero extra round trips. The session row is not deleted (suspension is non-destructive; unsuspending restores the same token).
func (*Store) BillingReport ¶ added in v0.2.1
BillingReport aggregates revenue and estimated cost per tenant over [from, to) (ADR-0054): every subscription overlapping the window contributes its monthly price snapshot un-prorated (label it as such when surfacing), and the usage ledger contributes summed quantities + estimated USD. Tenants with usage but no subscription (BYOK) appear with an empty PlanSlug.
func (*Store) CampaignNodeNames ¶ added in v0.5.0
func (s *Store) CampaignNodeNames(ctx context.Context, campaignID uuid.UUID) ([]IndexableNode, error)
CampaignNodeNames returns every Node in a Campaign as (id, name).
gm_private entries are INCLUDED. An appearance is GM-facing retrieval, never a prompt read: the GM's own secret being mentioned at the table is exactly the kind of thing they want to find again, and hiding it here would make the index lie about their own campaign.
func (*Store) CampaignTags ¶ added in v0.5.0
CampaignTags returns every (node, tag) pair in a Campaign, ordered so the client can build both the tag vocabulary and the per-node index from one read.
func (*Store) CancelPendingVoiceSessionControl ¶ added in v0.4.0
CancelPendingVoiceSessionControl fails a still-pending control with 'requester timed out' — the requester's budget-expiry best-effort cancel. Fenced WHERE status='pending': a row the worker already finished is left untouched and false is returned (the worker won the race; the requester's honest "not confirmed" reply stands either way).
func (*Store) CharacterAgents ¶
CharacterAgents returns the Campaign's Character NPC Agents (agent_role = 'character'), excluding the auto-created Butler. The live voice slice has exactly one (the seeded NPC); #6's web app lists many.
func (*Store) ClaimJob ¶
ClaimJob atomically claims the single oldest runnable job in any of kinds and returns it in its post-claim state (status='running', attempts incremented, leased_until = now()+lease). Runnable means a pending job whose run_after has arrived, OR a running job whose lease has expired and still has attempts left (a crashed worker's abandoned lease). No runnable row yields ErrNotFound.
The claim is ONE atomic UPDATE whose target row is chosen by a `SELECT … FOR UPDATE SKIP LOCKED LIMIT 1` subquery: concurrent workers skip each other's locked candidates rather than block, so two workers claim two distinct jobs. The SKIP LOCKED sits on the SELECT subquery, never on the UPDATE.
func (*Store) ClaimVoiceSessionIntent ¶ added in v0.4.0
func (s *Store) ClaimVoiceSessionIntent(ctx context.Context, instanceID string) (VoiceSessionIntent, error)
ClaimVoiceSessionIntent atomically claims the single oldest PENDING intent for instanceID and returns it in its post-claim state (status='claimed', instance_id set, claimed_at/heartbeat_at = now()). No pending intent yields ErrNotFound.
The claim is ONE atomic UPDATE whose target is chosen by a `SELECT … FOR UPDATE SKIP LOCKED LIMIT 1` subquery, so two concurrent workers skip each other's locked candidates and claim two DISTINCT intents (never the same one) — exactly the job-runner idiom (ADR-0049). It claims 'pending' ONLY: a 'claimed'/'live' row whose worker crashed is NEVER re-claimed here (ADR-0006/0057 (e) — no mid-session takeover); ReapDeadVoiceSessionIntents marks such a row 'dead' instead, and the Tenant restarts.
func (*Store) CloseVoiceSession ¶
func (s *Store) CloseVoiceSession(ctx context.Context, id uuid.UUID, status VoiceSessionStatus, lineCount int, endReason *string) (VoiceSession, error)
CloseVoiceSession closes a running Voice Session with an explicit terminal status and end_reason: it sets ended_at=now(), status, the final line_count, and end_reason (NULL when endReason is nil), returning the updated row. A missing id yields ErrNotFound. It is the single terminal-write seam (#123): [EndVoiceSession] delegates to it for a clean stop ('ended', NULL reason), and the session Manager calls it directly with 'failed' + the readable cause on a fatal gateway rejection.
func (*Store) CompleteJob ¶
CompleteJob marks the caller's claimed generation of a job done and clears its lease. attempts is the claimed Job's attempts (the claim generation). A missing id OR a superseded claim (a newer lease bumped attempts) yields ErrNotFound.
func (*Store) CountJobBacklog ¶
CountJobBacklog returns, per kind, the number of currently-runnable jobs — the same runnable predicate ClaimJob uses (pending-and-due, or running-with-an- expired-lease-and-attempts-left). It backs the backlog gauge (ADR-0032). A kind with no runnable jobs is absent from the map (the caller reads it as zero).
func (*Store) CountTranscriptLines ¶
CountTranscriptLines returns the number of persisted Lines for a Voice Session — the authoritative line_count the summary records on Stop (#74): rows == distinct lines, so it matches the persisted history. An unknown session is 0.
func (*Store) CountUnembeddedChunks ¶
CountUnembeddedChunks returns the number of Transcript Chunks still awaiting an embedding (embedding IS NULL) — the embedding-backlog gauge value (#104). It is process-wide (no tenant/campaign filter): ADR-0032 keeps that cardinality off the metric, so the gauge is a single global number.
func (*Store) CountUnembeddedNodes ¶ added in v0.2.1
CountUnembeddedNodes returns the number of Knowledge Graph Nodes still awaiting an embedding (embedding IS NULL) — the node embedding-backlog gauge value (#300, ADR-0032), mirroring CountUnembeddedChunks. Process-wide (no campaign filter) to keep the metric's cardinality bounded.
func (*Store) CountUnembeddedNodesInCampaign ¶ added in v0.5.0
func (s *Store) CountUnembeddedNodesInCampaign(ctx context.Context, campaignID uuid.UUID) (int, error)
CountUnembeddedNodesInCampaign counts a Campaign's Nodes still awaiting an embedding (#536). The probable-duplicate scan cannot see them, so the health panel reports the count rather than letting "no duplicates" imply a clean bill of health the check could not give.
Distinct from Store.CountUnembeddedNodes, which is the process-wide gauge (kept campaign-free to bound metric cardinality, ADR-0032).
func (*Store) CreateAgent ¶
CreateAgent inserts an Agent and returns its generated ID. Inserting a second Butler in a Campaign violates the partial-unique index (ADR-0009). A Character is assigned the next round-robin speaker-colour slot for its Campaign (stable once stored); the Butler keeps slot 0.
func (*Store) CreateAgentWithNPCNode ¶ added in v0.3.0
CreateAgentWithNPCNode creates an Agent and — for a Character — its linked NPC Knowledge Graph Node in ONE transaction (#479, ADR-0008 second amendment): every new Character NPC starts with a wiki entry named after it, carrying the agent_id "voiced by" link. The Node starts with an empty body and gm_private=false (the Persona is NOT copied — persona is how the character speaks, the Node body is what the world knows). A non-character role creates no Node. Deleting the Agent later leaves the Node as a normal wiki-only entry (agent_id ON DELETE SET NULL).
func (*Store) CreateBoard ¶ added in v0.5.0
func (s *Store) CreateBoard(ctx context.Context, campaignID uuid.UUID, name string) (KGBoard, error)
CreateBoard makes an empty prep board.
func (*Store) CreateCampaign ¶
CreateCampaign inserts a Campaign and returns its generated ID. The auto-Butler trigger (ADR-0009) inserts the campaign's 'Glyphoxa' Butler as a side effect.
func (*Store) CreateCharacter ¶
CreateCharacter inserts a Player Character into a Campaign and returns its id. A second Character for the same (campaign, discord_user_id) violates the unique index and yields ErrConflict (one Character per Discord User per Campaign).
func (*Store) CreateEdge ¶
CreateEdge inserts a typed directional Edge after checking both endpoints exist in the given Campaign and the (type, from-type, to-type) combination is valid. A self-edge is rejected up front (ErrInvalidEdge); a missing or cross-campaign endpoint yields ErrNotFound (the endpoint SELECT is Campaign-scoped, so a foreign Node is simply invisible); an invalid combination yields ErrInvalidEdge; a duplicate (from, to, type) yields ErrConflict. Validation on the immutable node_type is sound without a trigger because a Node's type never changes.
func (*Store) CreateHighlight ¶
CreateHighlight inserts one detected highlight as a 'candidate' (the Saver's worker calls it after the clip is stored, #308). id/status/clip metadata are caller-supplied so the row and its blob.Key agree; empty SpeakerIDs are stored as the empty array (never NULL). A blank Status defaults to 'candidate'.
func (*Store) CreateKnowledgeProposal ¶
func (s *Store) CreateKnowledgeProposal(ctx context.Context, campaignID, agentID uuid.UUID, proposedWrite []byte) error
CreateKnowledgeProposal inserts a pending proposal and returns the persisted row. proposedWrite is the raw jsonb payload (the caller marshals it). This layer does NO dedup by design: exact/normalized write-time dedup lives one layer up in the Tool handler (pkg/tool remember_knowledge, #411), which suppresses a repeat BEFORE calling here; genuine near-duplicates that differ in wording still land side-by-side for the GM to merge or reject (ADR-0052: similarity is a hint, not a semantic judgment — no auto-merge).
func (*Store) CreateMap ¶ added in v0.5.0
func (s *Store) CreateMap(ctx context.Context, m NewCampaignMap) (CampaignMap, error)
CreateMap inserts a Map and returns the persisted row. The caller writes the blob first: a row that references missing bytes is worse than an orphaned blob, which the seam's reconciliation can find.
func (*Store) CreateNode ¶
CreateNode inserts a Knowledge Graph Node and returns the persisted row. The node_type is cast server-side to the kg_node_type enum, so an out-of-enum type is rejected by Postgres rather than silently stored.
func (*Store) CreateNodeWithAspects ¶ added in v0.5.0
func (s *Store) CreateNodeWithAspects(ctx context.Context, n NewKGNode, aspects []NewKGNodeAspect) (KGNode, error)
CreateNodeWithAspects creates a Node and its Aspects in ONE transaction (#542). Two statements would leave a created entry with no aspects when the second failed — and the client, seeing only an opaque error, cannot tell whether to retry (duplicating the entry) or not.
func (*Store) CreatePin ¶ added in v0.5.0
CreatePin pins a Node onto a Map. A duplicate (map, node) yields ErrConflict — one Pin per Node per Map, though the same Node may be pinned on several Maps (the city map AND the tavern floor plan). A Node or Map from another Campaign is refused by the composite FKs and yields ErrNotFound.
func (*Store) CreatePlanningThread ¶ added in v0.8.0
func (s *Store) CreatePlanningThread(ctx context.Context, campaignID uuid.UUID, title string) (PlanningThread, error)
CreatePlanningThread inserts a thread for the campaign and returns it. title may be empty — the first exchange auto-fills it.
func (*Store) CreateProviderConfig ¶
CreateProviderConfig inserts a Provider Config and returns its generated ID.
func (*Store) CreateSession ¶
CreateSession inserts a session row and returns it.
func (*Store) CreateTenant ¶
CreateTenant inserts a Tenant and returns its generated ID.
func (*Store) CreateToolGrant ¶
CreateToolGrant inserts a Tool Grant and returns its generated ID. An empty Config is stored as SQL NULL. A duplicate (agent_id, tool_name, surface) violates the UNIQUE index — an Agent grants a Tool at most once per surface (ADR-0029, ADR-0062).
func (*Store) CreateVoiceSession ¶
CreateVoiceSession opens a Voice Session for a Campaign: it INSERTs a row with status='running' and started_at=now() and returns it. The SessionManager holds the returned id to End the session on Stop.
func (*Store) CreateVoiceSessionControl ¶ added in v0.4.0
func (s *Store) CreateVoiceSessionControl(ctx context.Context, c VoiceSessionControl) (VoiceSessionControl, error)
CreateVoiceSessionControl writes a pending control row for an intent and returns it. The requester then polls GetVoiceSessionControl until the hosting worker writes a terminal status.
func (*Store) CreateVoiceSessionIntent ¶ added in v0.4.0
func (s *Store) CreateVoiceSessionIntent(ctx context.Context, tenantID, campaignID uuid.UUID, voiceChannelID string) (VoiceSessionIntent, error)
CreateVoiceSessionIntent writes a 'pending' claim-plane row for a Tenant's Campaign and returns it. voiceChannelID carries the start's explicit voice-channel pick across the plane; empty means the worker uses the guild's Default Voice Channel. A second create while the Tenant already has a non-terminal (pending/claimed/live) intent trips the one-live-per-tenant partial UNIQUE index (23505) and yields ErrIntentActive — the per-Tenant single-active guard, now durable in the DB rather than the in-process Manager.
func (*Store) DeleteAgent ¶
DeleteAgent removes a Character NPC by id, scoped to its owning Campaign (#342): every clause matches (id, campaign_id), so an Agent in another Campaign is invisible — it neither exists nor deletes — and yields ErrNotFound, refusing a cross-campaign delete. Deleting a Butler is rejected with ErrButlerUndeletable (ADR-0009): the guarded DELETE leaves a Butler row untouched, and the wrapping CTE reports whether the id existed in the Campaign and whether it was a Butler in one atomic round-trip — so a missing id yields ErrNotFound and a Butler yields ErrButlerUndeletable, distinct from "deleted nothing".
func (*Store) DeleteBoard ¶ added in v0.5.0
DeleteBoard removes a prep board and its entries. The ENTRIES themselves are untouched — a board is a shortlist, not ownership.
func (*Store) DeleteCampaign ¶
DeleteCampaign permanently removes an ALREADY-ARCHIVED campaign (#269). The single DELETE cascades to everything owned by the campaign — Agents (and their Tool Grants), Knowledge Graph Nodes/Edges, Voice Sessions (and their Transcript Lines), and Transcript Chunks — via the ON DELETE CASCADE foreign keys the schema already declares (00001 agents/transcript_chunk, 00006 voice_sessions, 00007 transcript_line, 00010 kg_node, 00012 kg_edge, 00013 tool_agent_grant); users.active_campaign_id is nulled via its ON DELETE SET NULL (00014). The Butler is removed through the agents CASCADE, deliberately NOT through DeleteAgent's butler guard (ADR-0009): a campaign delete takes its Butler with it. The WHERE archived_at IS NOT NULL clause makes the delete refuse a non-archived campaign; when no row is affected, GetCampaignInTenant disambiguates a missing campaign (ErrNotFound) from a live one (ErrNotArchived). This is irrecoverable removal of play history including transcript PII — no soft-delete retention window (#265).
It is TENANT-SCOPED (#473): the DELETE matches (id, tenant_id), so a foreign-tenant id is invisible — it disambiguates to ErrNotFound (never ErrNotArchived, which would confirm the id exists), and a cross-tenant delete can never cascade away the victim's play history.
func (*Store) DeleteCampaignWithJob ¶
func (s *Store) DeleteCampaignWithJob(ctx context.Context, tenantID, id uuid.UUID, jobKind string, jobPayload []byte) error
DeleteCampaignWithJob hard-deletes an archived campaign AND enqueues a follow-up job in the SAME transaction (#308, ADR-0048/0049): the job row exists if and only if the delete committed. This closes the blob-orphan window a delete-then-enqueue would open — a refused/failed delete never leaves a sweep that would drop a surviving campaign's clips, and a crash right after the delete never loses the sweep. The delete's error mapping (ErrNotFound / ErrNotArchived) is unchanged. jobPayload must be non-empty; callers with nothing to sweep use [DeleteCampaign]. It is TENANT-SCOPED (#473): the delete matches (id, tenant_id).
func (*Store) DeleteCharacter ¶
DeleteCharacter removes a Character by id, scoped to its owning Campaign (#342): the DELETE matches (id, campaign_id), so a Character in another Campaign is not deleted and yields ErrNotFound — a cross-campaign delete is refused server-side. A missing id likewise yields ErrNotFound so the RPC can distinguish "gone" from "never existed".
func (*Store) DeleteEdge ¶
DeleteEdge removes a typed Edge by id, scoped to its owning Campaign (#342): the DELETE matches (id, campaign_id), so an Edge in another Campaign is not deleted and yields ErrNotFound — a cross-campaign delete is refused server-side. A missing id likewise yields ErrNotFound.
func (*Store) DeleteHighlight ¶
DeleteHighlight removes one highlight within the tenant and returns its clip_key so the caller can drop the blob through the seam (ADR-0048). A missing id (or foreign tenant) yields ErrNotFound. The blob delete is the caller's responsibility and runs BEFORE this in the RPC (blob-then-row), but the key is returned so a delete driven off a prior read still has it.
func (*Store) DeleteMap ¶ added in v0.5.0
func (s *Store) DeleteMap(ctx context.Context, campaignID, id uuid.UUID) (blobKey string, err error)
DeleteMap removes a Map and returns the blob key the caller must then delete through the seam (ADR-0048: deletion goes through the seam, not FK cascade). Returning the key rather than deleting the blob here keeps storage free of a blob dependency, and makes the caller's obligation explicit.
Child Maps are NOT cascaded: parent_map_id is ON DELETE SET NULL, so deleting a middle map lifts its children to the top level instead of silently taking a subtree with it.
func (*Store) DeleteNode ¶
func (s *Store) DeleteNode(ctx context.Context, campaignID, id uuid.UUID) (portraitBlobKey string, err error)
DeleteNode removes a Knowledge Graph Node by id, scoped to its owning Campaign (#342): the DELETE matches (id, campaign_id), so a Node in another Campaign is not deleted and yields ErrNotFound — a cross-campaign delete is refused. A missing id likewise yields ErrNotFound so the RPC can distinguish "gone" from "never existed".
It returns the deleted Node's portrait blob key (” when it had none) so the caller can release the bytes through the seam (#590, ADR-0048) — after the DELETE, nothing in the database names them, mirroring DeleteMap.
func (*Store) DeletePin ¶ added in v0.5.0
DeletePin unpins a Node from a Map, scoped to its Campaign.
func (*Store) DeletePlanningThread ¶ added in v0.8.0
DeletePlanningThread removes a thread and (via the composite FK cascade) its messages. A thread outside the campaign yields ErrNotFound.
func (*Store) DeleteSession ¶
DeleteSession removes a session row by token (logout / revocation). Deleting a token that no longer exists is not an error — logout is idempotent.
func (*Store) DeleteSessionCandidates ¶
DeleteSessionCandidates removes every remaining CANDIDATE highlight row for a Voice Session (the 7-day purge, #308/ADR-0051), returning how many rows were deleted. Promoted rows are untouched. Idempotent: a second run deletes nothing. The caller drops the blobs first via ListSessionCandidateClipKeys.
func (*Store) DeleteTapeConsent ¶
func (s *Store) DeleteTapeConsent(ctx context.Context, campaignID uuid.UUID, discordUserID string) error
DeleteTapeConsent revokes a Speaker's tape consent for a Campaign (#306). It is idempotent — deleting an absent row is a no-op — so a double revoke is harmless.
func (*Store) DeleteToolGrant ¶
DeleteToolGrant removes an Agent's VOICE grant of the named Tool. Deleting a grant that is not present yields ErrNotFound (so the caller can tell "removed" from "was never there"). Removing the row is how a GM revokes a Tool: after hydration the Agent's GrantSet no longer carries it, so the LLM is never shown the Tool and cannot call it. Voice-only keeps the pre-0062 meaning for every caller (the grant editor RPC, the bundle importer); chat rows have no mutation surface in v1 — they are the trigger-seeded defaults.
func (*Store) DeleteTranscriptLine ¶ added in v0.5.0
DeleteTranscriptLine removes one transcript Line by its replay key — the reconciliation half of the LINE grain's delivered-only invariant (#437, ADR-0040 amendment 2026-07-22). The Relay persists optimistically at TTSInvoked and calls this through the SAME single-writer queue when the turn ends having delivered zero sentences, so replay never shows text the room never heard. Deleting a row that is not there is NOT an error: the reconcile is best-effort and may race a dropped (queue-full) UPSERT that never landed.
func (*Store) EndTenantPlan ¶ added in v0.2.1
EndTenantPlan ends a Tenant's active subscription (cancellation). ErrNotFound when the tenant has no active subscription.
func (*Store) EndVoiceSession ¶
func (s *Store) EndVoiceSession(ctx context.Context, id uuid.UUID, lineCount int) (VoiceSession, error)
EndVoiceSession closes a running Voice Session cleanly: status='ended' with a NULL end_reason and the final line_count, returning the updated row. A missing id yields ErrNotFound. It is a thin wrapper over [CloseVoiceSession] — the clean-stop path that leaves end_reason NULL (distinct from orphaned/failed).
func (*Store) EnqueueJob ¶
func (s *Store) EnqueueJob(ctx context.Context, kind string, payload []byte, maxAttempts int) (uuid.UUID, error)
EnqueueJob inserts a pending job of the given kind carrying payload (jsonb), returning its id. A non-positive maxAttempts takes DefaultJobMaxAttempts. The job is immediately runnable (run_after defaults to now()).
func (*Store) EnqueueJobAt ¶
func (s *Store) EnqueueJobAt(ctx context.Context, kind string, payload []byte, maxAttempts int, runAfter time.Time) (uuid.UUID, error)
EnqueueJobAt is EnqueueJob with an explicit run_after: the job stays pending (not runnable) until runAfter arrives. It backs deferred work like the Session Highlights 7-day candidate purge (#308, ADR-0051/0049). A zero runAfter falls back to now() (immediately runnable), matching EnqueueJob.
func (*Store) FindCampaignByName ¶
func (s *Store) FindCampaignByName(ctx context.Context, tenantID uuid.UUID, name string) (Campaign, error)
FindCampaignByName returns the Tenant's Campaign with the given name, or ErrNotFound. It returns the full row rather than just the ID so a caller wiring a Voice Session gets the Campaign Language (the matcher's phonetic scheme, #199) from the same lookup.
func (*Store) FindTenantByName ¶
FindTenantByName returns the Tenant with the given name, or ErrNotFound. Used by the idempotent seed to detect an already-seeded database.
func (*Store) FinishVoiceSessionControl ¶ added in v0.4.0
func (s *Store) FinishVoiceSessionControl(ctx context.Context, id uuid.UUID, status VoiceSessionControlStatus, resultIDs []string, lastError string) (VoiceSessionControl, error)
FinishVoiceSessionControl writes a control's terminal state (done/failed) with its result ids / last_error and ended_at = now(). Fenced WHERE status='executing' (the worker must have claimed it via StartVoiceSessionControl first) so a lost race (the sweep won a stale executing row) yields ErrNotFound and the caller does not overwrite a settled row.
func (*Store) FinishVoiceSessionIntent ¶ added in v0.4.0
func (s *Store) FinishVoiceSessionIntent(ctx context.Context, id uuid.UUID, instanceID string, status VoiceSessionIntentStatus, lastError string) (VoiceSessionIntent, error)
FinishVoiceSessionIntent writes a terminal state (done/failed/dead) for instanceID's intent with a recorded last_error and ended_at = now(). Fenced by (id, instance_id) and a non-terminal current status so a superseded caller (the reaper already marked it dead) matches no row and yields ErrNotFound. The worker calls it once its local session has fully wound down.
func (*Store) FirstLineIDAtOrAfter ¶ added in v0.8.0
func (s *Store) FirstLineIDAtOrAfter(ctx context.Context, voiceSessionID uuid.UUID, at time.Time) (string, error)
FirstLineIDAtOrAfter returns the line_id of a Voice Session's earliest Transcript Line at or after the given time — the deep-link anchor a semantic Transcript Chunk search hit resolves to (#591): a chunk is the 3–6-utterance retrieval grain (ADR-0011) with no line identity of its own, so the palette anchors on the first Line the chunk's window covers. Ordered by (ts, seq) so two Lines sharing a timestamp resolve deterministically. No Line at/after the time (a chunk flushed after the last persisted Line) is ErrNotFound — the caller renders the hit without a scroll target, not an error.
func (*Store) FirstTenant ¶
FirstTenant returns the earliest-created Tenant, or ErrNotFound when the DB holds none. The `glyphoxa seed -bundle` path uses it to land a bundle beside an already-provisioned Tenant instead of minting a duplicate one (ADR-0053): the ordering is deterministic (created_at then id) so a multi-tenant DB always resolves the same Tenant.
func (*Store) GetActiveCampaign ¶
GetActiveCampaign returns the "active" campaign: the most-recently-created one, GLOBALLY. It is the deliberately tenant-FREE most-recent fallback the standalone voice node's boot path uses (cmd/glyphoxa, ADR-0039 single-operator): with one Tenant the global latest IS that Tenant's latest. The web/RPC tier uses Store.GetActiveCampaignInTenant instead so a stranger never falls through to another tenant's latest campaign (#473). Archived campaigns are excluded from this fallback (#269): an only-archived DB resolves to ErrNotFound, so an archived campaign can never be the implicit Active Campaign nor start a Voice Session. No campaign yields ErrNotFound (the RPC layer maps it to Connect CodeNotFound).
func (*Store) GetActiveCampaignForUserInTenant ¶ added in v0.3.0
func (s *Store) GetActiveCampaignForUserInTenant(ctx context.Context, tenantID uuid.UUID, discordUserID string) (Campaign, error)
GetActiveCampaignForUserInTenant is the tenant-scoped durable-selection read (#473, and the ONLY per-operator durable read after #490 removed the tenant-free GetActiveCampaignForUser — every slash/RPC caller now carries a resolved Tenant, so a tenant-free read can no longer be grabbed by the next handler): it resolves the operator's active_campaign_id ONLY when the selected campaign is in the caller's tenant (`AND c.tenant_id = $2`). A selection pointing at another tenant's campaign — which SetActiveCampaign's FK-only write cannot prevent by itself — reads back as ErrNotFound here, so it can never pivot a stranger's campaign-scoped surfaces onto the victim tenant (self-signup design §0a). The standalone voice node uses the context-free GetOperatorActiveCampaign instead. Absent-selection semantics: no row / no selection / deleted (FK ON DELETE SET NULL) / archived (#269) → ErrNotFound, so the caller falls through to the GetActiveCampaignInTenant most-recent fallback. The columns are qualified to `c` because users and campaign share id/name/created_at/updated_at.
func (*Store) GetActiveCampaignInTenant ¶ added in v0.3.0
func (s *Store) GetActiveCampaignInTenant(ctx context.Context, tenantID uuid.UUID) (Campaign, error)
GetActiveCampaignInTenant returns the tenant's most-recently-created active campaign, or ErrNotFound (#473) — the tenant-scoped most-recent fallback the web/RPC Active-Campaign resolution walks last. The `WHERE tenant_id = $1` guard keeps a caller from ever falling through to another tenant's latest campaign. Archived campaigns are excluded (#269), matching GetActiveCampaign.
func (*Store) GetAdmissionPosture ¶ added in v0.3.0
GetAdmissionPosture returns the recorded Admission Mode ('allowlist' or 'open'), or ErrNotFound when no posture has ever been recorded (a pre-0055 deployment's first boot). The value is stored verbatim; vocabulary validation lives in the auth tier (auth.ParseAdmissionMode).
func (*Store) GetCampaign ¶
GetCampaign loads one Campaign by id, or ErrNotFound. It is the tenant-FREE read the standalone voice node / slash surface, the bundle export/import, and the recap engine use — surfaces that carry a server-derived campaign id, never a client-supplied one (ADR-0039 single-operator pass-through). The web/RPC tier that takes a client id uses Store.GetCampaignInTenant instead so a foreign id can never resolve. It backs the /glyphoxa use resolution step that turns a live Voice Session's campaign_id back into the full Campaign (#108).
func (*Store) GetCampaignInTenant ¶ added in v0.3.0
GetCampaignInTenant loads one Campaign by id WITHIN the tenant, or ErrNotFound (#473). The `AND tenant_id = $2` guard means a campaign owned by another tenant reads back as absent — never a permission-style error that would confirm the id exists (self-signup design §0a). It is the web/RPC-tier read: every handler that takes a client-supplied campaign id resolves it through here, threading auth.TenantID(ctx) down, so cross-tenant reads/writes are refused at the query.
func (*Store) GetCampaignShareChannel ¶ added in v0.2.1
GetCampaignShareChannel returns the Campaign's remembered highlight-share text channel id, or "" when none has been chosen yet. An unknown campaign is ErrNotFound (distinct from the empty-string "never shared" state of a known one).
func (*Store) GetCharacterByDiscordUser ¶
func (s *Store) GetCharacterByDiscordUser(ctx context.Context, campaignID uuid.UUID, discordUserID string) (Character, error)
GetCharacterByDiscordUser resolves the Character a Discord User plays in a Campaign, or ErrNotFound. It is the speaker → Character lookup Address Detection / transcript attribution consume (#281). The (campaign_id, discord_user_id) unique index guarantees at most one row, so a cross-campaign lookup for the same Discord User simply misses.
func (*Store) GetDeploymentConfig ¶
func (s *Store) GetDeploymentConfig(ctx context.Context, tenantID uuid.UUID) (DeploymentConfig, error)
GetDeploymentConfig loads a Tenant's deployment config, or ErrNotFound when nothing has been saved yet (the Configuration screen treats that as the empty, key-needed state).
func (*Store) GetEmbeddingsProviderConfig ¶
func (s *Store) GetEmbeddingsProviderConfig(ctx context.Context) (ProviderConfig, error)
GetEmbeddingsProviderConfig returns the most-recently-updated 'embeddings' Provider Config, or ErrNotFound when none is bound. Process-wide (no tenant filter), mirroring GetActiveCampaign's single-operator posture (ADR-0039): the backfill worker resolves ONE embeddings provider for the process. Not-found is not fatal — the worker falls back to the local Ollama default (ADR-0004/0011).
func (*Store) GetHighlight ¶
GetHighlight loads one highlight by id within the tenant, or ErrNotFound. The tenant guard means a foreign-tenant id reads as absent (never leaked).
func (*Store) GetJob ¶
GetJob loads one job by id, or ErrNotFound. It backs dead-letter inspection and tests; the runner itself never reads a job back after acting on it.
func (*Store) GetLatestVoiceSession ¶
func (s *Store) GetLatestVoiceSession(ctx context.Context, campaignID uuid.UUID) (VoiceSession, error)
GetLatestVoiceSession returns a Campaign's most-recently-started Voice Session, or ErrNotFound when none has ever run. It backs the Session screen's idle last-session summary (#72): when no session is active, the screen shows when the prior session ended and its line count.
func (*Store) GetLiveVoiceSessionIntentForTenant ¶ added in v0.4.0
func (s *Store) GetLiveVoiceSessionIntentForTenant(ctx context.Context, tenantID uuid.UUID) (VoiceSessionIntent, error)
GetLiveVoiceSessionIntentForTenant returns the Tenant's current non-terminal (pending/claimed/live) intent, or ErrNotFound when the Tenant has none — the per-Tenant read backing IntentControl.Active (the split-mode sibling of Manager.Active). The one-live-per-tenant index guarantees at most one row matches.
func (*Store) GetMap ¶ added in v0.5.0
GetMap loads one Map scoped to its Campaign (#342). A Map in another Campaign yields ErrNotFound.
func (*Store) GetOperatorActiveCampaign ¶
GetOperatorActiveCampaign returns the durable Active Campaign selection of the single operator, for a surface with NO logged-in user context — the standalone voice node (#323, ADR-0039 single-operator pass-through). It is the context-free analogue of GetActiveCampaignForUserInTenant: it joins any users row carrying a non-null active_campaign_id to a non-archived campaign. Single operator, so at most one such selection is meaningful; a tie breaks on the most-recently-updated users row — the freshest login-or-selection, NOT strictly the freshest campaign selection: UpsertUser bumps users.updated_at on EVERY OAuth login, not only on SetActiveCampaign, so `ORDER BY u.updated_at` is a login clock, not a selection clock. Acceptable under the single-operator model (ADR-0039), where at most one operator has a durable selection anyway. ErrNotFound when no operator has a durable, non-archived selection (deleted/archived selections are treated as absent, matching GetActiveCampaignForUserInTenant), so the caller falls through to the GetActiveCampaign recent fallback.
func (*Store) GetPartyMarker ¶ added in v0.5.0
func (s *Store) GetPartyMarker(ctx context.Context, campaignID, sessionID uuid.UUID) (PartyMarker, error)
GetPartyMarker reads a Voice Session's marker, joined to the Map and Pin it names so a caller can render or describe it without further reads.
A marker whose Map or Pin has been deleted reads as UNSET rather than dangling: the FKs are ON DELETE SET NULL, so the row degrades to "no marker" on its own.
func (*Store) GetPendingKnowledgeProposal ¶ added in v0.2.1
func (s *Store) GetPendingKnowledgeProposal(ctx context.Context, campaignID, id uuid.UUID) (KnowledgeProposal, error)
GetPendingKnowledgeProposal loads a single PENDING proposal by id, scoped to its Campaign and joined to its authoring Agent's name (#300, ADR-0052). An already-reviewed or missing id — or one in another Campaign — yields ErrNotFound; the review surface uses it to build the similarity-hint query for a live proposal only.
func (*Store) GetPin ¶ added in v0.5.0
GetPin loads one Pin with its joined Node fields, scoped to its Campaign.
func (*Store) GetPlanBySlug ¶ added in v0.3.0
GetPlanBySlug returns the plan with slug, archived or not — the read stays faithful and the caller decides whether archived is a refusal (the ADR-0055 open-mode boot preflight does exactly that). ErrNotFound for an unknown slug.
func (*Store) GetPlanningThread ¶ added in v0.8.0
func (s *Store) GetPlanningThread(ctx context.Context, campaignID, id uuid.UUID) (PlanningThread, error)
GetPlanningThread loads one thread scoped to its campaign; a thread in another campaign yields ErrNotFound, never a permission error.
func (*Store) GetProviderConfig ¶
GetProviderConfig loads one Provider Config by id.
func (*Store) GetProviderConfigByComponent ¶
func (s *Store) GetProviderConfigByComponent(ctx context.Context, tenantID uuid.UUID, component Component) (ProviderConfig, error)
GetProviderConfigByComponent returns the Tenant's most-recently-updated Provider Config for a Component, or ErrNotFound when none is bound. A Component can have more than one Provider in the matrix (ADR-0004); this resolves the one the operator last saved.
func (*Store) GetTenant ¶ added in v0.3.0
GetTenant returns the Tenant with the given id, or ErrNotFound. The AuthService's GetCurrentUser uses it to serve the bound Tenant's display name (ADR-0055).
func (*Store) GetTenantIDByGuildID ¶ added in v0.4.0
GetTenantIDByGuildID resolves a Discord Guild snowflake to the Tenant that configured it — the interaction→Tenant routing read (#490): an inbound slash interaction carries its Guild, and this maps it to the owning Tenant before any storage read touches campaigns, so a command only ever reaches its own Tenant's data. An unknown Guild returns ErrNotFound, which the Gate maps to a clean ephemeral rejection.
Since #483 a guild_id is bound by at most ONE Tenant (the first-registrar-wins partial UNIQUE index deployment_config_guild_owner; SaveDiscordChannels rejects a second Tenant's bind with ErrGuildTaken), so this read is unambiguous by construction for every Guild→Tenant consumer — the interaction router here AND the member-picker path (presence.Clients.VoiceChannelMembers). The ORDER BY is kept as harmless belt-and-braces against a pre-index legacy duplicate.
func (*Store) GetTenantSpendCaps ¶
GetTenantSpendCaps loads a Tenant's soft/hard spend caps, each nil when that cap is unset. ErrNotFound when no tenant row exists for the id — distinct from a tenant that exists with both caps NULL (which returns a zero-value SpendCaps).
func (*Store) GetUserByDiscordID ¶
GetUserByDiscordID loads a user by Discord snowflake, or ErrNotFound.
func (*Store) GetVoiceSession ¶
GetVoiceSession loads one Voice Session by id, or ErrNotFound.
func (*Store) GetVoiceSessionControl ¶ added in v0.4.0
func (s *Store) GetVoiceSessionControl(ctx context.Context, id uuid.UUID) (VoiceSessionControl, error)
GetVoiceSessionControl loads one control row by id, or ErrNotFound — the requester's poll read.
func (*Store) GetVoiceSessionIntent ¶ added in v0.4.0
func (s *Store) GetVoiceSessionIntent(ctx context.Context, id uuid.UUID) (VoiceSessionIntent, error)
GetVoiceSessionIntent loads one intent by id, or ErrNotFound. It backs the IntentControl poll (the web tier watching a Start it wrote) and tests.
func (*Store) HasPresenceOwner ¶ added in v0.4.0
HasPresenceOwner reports whether ANY presence-owner claim row exists — live or stale. It backs the -mode all mixed-deployment boot guard (#483 M3): an owner row (even an expired one) is proof a claim-plane voice fleet has been driving this database, and an all-mode process must refuse to join it (its broad boot reconcile would close live workers' rows, and its intent-less sessions break the one-live-per-tenant invariant).
func (*Store) HeartbeatVoiceSessionIntent ¶ added in v0.4.0
func (s *Store) HeartbeatVoiceSessionIntent(ctx context.Context, id uuid.UUID, instanceID string) (stopRequested bool, err error)
HeartbeatVoiceSessionIntent stamps heartbeat_at = now() for instanceID's live (or claimed) intent and reports whether a stop was requested, so the owning worker learns on each beat whether the web tier asked it to wind down. It is fenced WHERE instance_id=$2 AND status IN ('claimed','live'): a row the reaper already marked dead (worker declared stale), or one now owned by another instance, matches nothing and yields ErrNotFound — the caller reads that as "my claim was superseded" and kills its local session (ADR-0006: it must not keep running a session the plane believes is dead).
func (*Store) HighlightsExist ¶ added in v0.2.1
HighlightsExist reports which of the given Highlight ids still have a row, returning a set (present ids map to true; absent ids are simply not in the map). It is the membership half of the boot orphan-image sweep's anti-join (#421): the sweep enumerates image blobs THROUGH the blob seam (blob.Store.List), extracts each blob's Highlight id, then asks this which of them still exist — the blobs whose id is absent are the delete-vs-enrich orphans. Keeping the anti-join in Go (not a SELECT against the blob table) is what keeps the orphan sweep working across a blob-backend swap (ADR-0048). An empty input runs no query. Process-wide, carries no tenant (the ids are globally unique PKs).
func (*Store) ImportVoiceSession ¶
ImportVoiceSession inserts a historical Voice Session from a Campaign Bundle (#292, ADR-0053): unlike [CreateVoiceSession], which mints a fresh 'running' row with started_at=now(), this writes the bundle's started_at, ended_at, status, line_count and end_reason VERBATIM and returns the freshly minted id. The caller (bundle.Import) guarantees a terminal status (a non-terminal one is coerced to 'ended' upstream) and a non-nil ended_at, so no live loop ever owns an imported row. end_reason is written NULL when nil. It runs inside the import transaction, so a later failure rolls the row back with the rest.
func (*Store) InTx ¶
InTx runs fn against a Store bound to a single transaction, committing if fn returns nil and rolling back otherwise. Used by multi-row operations that must be atomic (e.g. the live-NPC seed).
On a Store ALREADY bound to a transaction (created by an enclosing InTx), it FLATTENS: fn runs against the same tx-bound Store, with no nested Begin and no savepoint (#291). This lets a method that uses InTx internally (e.g. CreateEdge) compose inside a larger import transaction. The caveat is that the inner call's atomicity then becomes the OUTER transaction's: an error raised after such an inner "commit" still rolls the whole outer tx back — there is no independent inner rollback boundary. That is exactly what the bundle importer wants (one all-or-nothing import), but a caller relying on partial-commit semantics from a nested InTx would be surprised.
func (*Store) InsertTranscriptChunk ¶
InsertTranscriptChunk writes one closed chunk and returns its generated id. The embedding is left NULL — the async pipeline fills it later (ADR-0011) — and the arrays default to empty (never NULL). embedding_model is left at its empty column default; the backfill worker (#116) stamps it when it embeds the row.
func (*Store) IsCampaignLiveIntent ¶ added in v0.4.0
IsCampaignLiveIntent reports whether campaignID has any non-terminal (pending/claimed/live) intent — the split-mode archive/delete live-guard (#491): the web tier drives no in-process session, so "is this Campaign live" is a claim-plane read rather than a Manager scan. Correct across the worker pool.
func (*Store) LastSpokenByAgent ¶ added in v0.5.0
func (s *Store) LastSpokenByAgent(ctx context.Context, campaignID uuid.UUID) ([]AgentLastSpoke, error)
LastSpokenByAgent returns, per Agent speaker label, the timestamp of its most recent committed Transcript Line in a Campaign (#544) — the "last spoke" prep signal. One grouped read for the whole roster rather than one per NPC.
Only Agent turns are considered (kind ∈ npc, butler); player and GM lines are a different question. Only committed Lines exist in this table at all (ADR-0012 / ADR-0040: partials are never persisted), so the answer is always about speech that actually reached the table.
It rides transcript_line_campaign_kind_idx (migration 00045). Without it this is a sequential scan of every tenant's transcript — the table had no campaign_id index because, until this read, nothing filtered on it.
func (*Store) ListAgents ¶
ListAgents returns all Agents in a Campaign (Butler + Character NPCs).
func (*Store) ListAllCampaigns ¶
ListAllCampaigns returns every Campaign — active AND archived — ordered by name (then id for a stable tie-break). It backs the archive-management panel's include_archived read (#269); the default list surfaces (ListCampaigns, the /glyphoxa use autocomplete) stay archive-excluding. It is the tenant-FREE list the standalone voice node uses (ADR-0039 single-operator); the web/RPC tier uses Store.ListAllCampaignsInTenant so the panel shows only the caller's own campaigns (#473).
func (*Store) ListAllCampaignsInTenant ¶ added in v0.3.0
func (s *Store) ListAllCampaignsInTenant(ctx context.Context, tenantID uuid.UUID) ([]Campaign, error)
ListAllCampaignsInTenant is the tenant-scoped archive-INCLUSIVE list (#473): the archive-management panel's read for the web/RPC tier, scoped to the caller's tenant so it never surfaces another tenant's archived campaigns.
func (*Store) ListBoards ¶ added in v0.5.0
ListBoards returns a Campaign's boards with their entries, in board order. One read for the whole set: a campaign has a handful of boards, and the Session screen wants them all at once.
func (*Store) ListCampaignHighlightClipKeys ¶
func (s *Store) ListCampaignHighlightClipKeys(ctx context.Context, campaignID uuid.UUID) ([]string, error)
ListCampaignHighlightClipKeys returns EVERY blob key a campaign hard-delete must sweep through the seam BEFORE the row cascade removes the highlights (ADR-0048): each highlight's clip_key AND its image_key (#311) AND its sound_key (#312) when non-empty — a fully enriched Highlight owns three blobs. No status filter: a campaign delete takes its highlights with it, kept or not. clip_key is always present; image_key and sound_key are UNION ALL'd only where set, so unenriched rows contribute one key.
func (*Store) ListCampaignMapBlobKeys ¶ added in v0.5.0
func (s *Store) ListCampaignMapBlobKeys(ctx context.Context, campaignID uuid.UUID) ([]string, error)
ListCampaignMapBlobKeys returns every Map image key a Campaign owns, for the hard-delete blob sweep (ADR-0048).
campaign_map cascades with the campaign row, so the keys are unlistable the instant the delete commits — they MUST be captured first, exactly as the Highlight clip keys are. Without this the images survive the campaign forever with nothing left in the database that names them: a delete that frees the rows and silently keeps the bytes, which is the failure ADR-0048's lifecycle rule exists to prevent.
func (*Store) ListCampaignPortraitKeys ¶ added in v0.8.0
func (s *Store) ListCampaignPortraitKeys(ctx context.Context, campaignID uuid.UUID) ([]string, error)
ListCampaignPortraitKeys returns every non-empty portrait blob key in a Campaign — the campaign hard delete's sweep input (#590), mirroring ListCampaignMapBlobKeys: captured BEFORE the delete, because the row cascade removes the only records that name the keys (ADR-0048).
func (*Store) ListCampaigns ¶
ListCampaigns returns every ACTIVE Campaign ordered by name (then id for a stable tie-break) — the /glyphoxa use autocomplete source (#108). Archived campaigns are excluded (#269): the autocomplete inherits that filter with no code change of its own. It is the tenant-FREE list the standalone voice node's slash surface uses (ADR-0039 single-operator); the web/RPC tier uses Store.ListCampaignsInTenant so the picker shows only the caller's own campaigns (#473). See ListAllCampaigns for the archive-inclusive read.
func (*Store) ListCampaignsInTenant ¶ added in v0.3.0
ListCampaignsInTenant is the tenant-scoped ACTIVE-only list (#473): the web/RPC picker source, showing only the caller's own campaigns (`WHERE tenant_id = $1`).
func (*Store) ListCharacters ¶
ListCharacters returns every Player Character in a Campaign in a stable display order (case-insensitive name, then id). An empty result is not an error.
func (*Store) ListDeploymentConfigs ¶ added in v0.4.0
func (s *Store) ListDeploymentConfigs(ctx context.Context) ([]DeploymentConfig, error)
ListDeploymentConfigs returns every saved deployment config, one per Tenant, ordered oldest-updated first for a deterministic boot seed. The per-tenant Discord client registry (#489, ADR-0010) reads this once at boot — before any request, with no tenant context — to stand up one standing client per distinct Bot token; each request-path read still narrows to a single Tenant via GetDeploymentConfig. An empty table returns a nil slice, not an error.
func (*Store) ListEdges ¶
ListEdges returns every Edge in a Campaign ordered (created_at, id) — the deterministic read the Campaign Bundle exporter serialises (#288, ADR-0053). It is Campaign-scoped (#342): only rows whose campaign_id matches are returned, so no Edge leaks across Campaigns. An empty Campaign yields an empty slice, not an error. Export is a rare admin read, so this reuses the (campaign_id) filter without a dedicated index (ADR-0053 decision).
func (*Store) ListGraphNodes ¶ added in v0.5.0
ListGraphNodes returns every Node in a Campaign in the display order ListNodes uses (type, case-insensitive name, id), projected for the graph view. The order is stable and content-independent, which is what lets the client's layout be a pure function of the payload.
This is a GM-FACING read: gm_private Nodes are included, because the Graph view is the GM's map of their own world. It is deliberately NOT part of PromptKGView — prompt assembly cannot reach it (#450).
func (*Store) ListHighlights ¶
func (s *Store) ListHighlights(ctx context.Context, tenantID, voiceSessionID uuid.UUID) ([]Highlight, error)
ListHighlights returns a Voice Session's highlights within the tenant, newest moment first (starts_at DESC, id DESC for a stable tie-break). It backs the GM session-end review UI (#309). An empty result is not an error.
func (*Store) ListMaps ¶ added in v0.5.0
ListMaps returns a Campaign's Maps in a stable display order (name, id). GM-facing: gm_private Maps are INCLUDED. The player-tier read is Store.ListPlayerMaps.
func (*Store) ListNodeAppearances ¶ added in v0.5.0
func (s *Store) ListNodeAppearances(ctx context.Context, campaignID, nodeID uuid.UUID, limit int) ([]AppearanceHit, error)
ListNodeAppearances returns an entry's most recent appearances, newest first.
The join is what makes the list useful: each row carries the line's own text and speaker, so the GM reads WHAT was said without following the link, and follows it only when they want the surrounding scene.
func (*Store) ListNodeAspects ¶ added in v0.5.0
func (s *Store) ListNodeAspects(ctx context.Context, campaignID, nodeID uuid.UUID) ([]KGNodeAspect, error)
ListNodeAspects returns one Node's Aspects in author order, INCLUDING private ones — a GM-facing read (the editor, the Campaign Bundle export). Prompt-facing code never calls it: it reads Aspects through the Node projections, whose aggregate filters gm_private in SQL.
func (*Store) ListNodes ¶
ListNodes returns every Knowledge Graph Node in a Campaign in a stable display order (node_type enum order, then case-insensitive name, then id) — the Knowledge panel's list. Each Node carries its Aspects, private ones INCLUDED: this is a GM-facing read and never flows into prompt assembly (#450, #542). An empty result is not an error.
func (*Store) ListPendingKnowledgeProposals ¶
func (s *Store) ListPendingKnowledgeProposals(ctx context.Context, campaignID uuid.UUID) ([]KnowledgeProposal, error)
ListPendingKnowledgeProposals returns a Campaign's pending proposals oldest-first (the review order), hitting the partial pending index. An empty queue yields an empty slice, not an error.
func (*Store) ListPendingVoiceSessionControls ¶ added in v0.4.0
func (s *Store) ListPendingVoiceSessionControls(ctx context.Context, intentID uuid.UUID) ([]VoiceSessionControl, error)
ListPendingVoiceSessionControls returns an intent's pending control rows in (created_at, id) order — the hosting worker's per-heartbeat drain scan. The order matters for 'say' (utterances must land in request order); mutes are idempotent so the queue is harmless for them.
func (*Store) ListPins ¶ added in v0.5.0
ListPins returns a Map's Pins in a stable order. GM-facing: every Pin is included, whatever its own or its Node's privacy. Store.ListPlayerPins is the player-tier read.
func (*Store) ListPlanningMessages ¶ added in v0.8.0
func (s *Store) ListPlanningMessages(ctx context.Context, campaignID, threadID uuid.UUID) ([]PlanningMessage, error)
ListPlanningMessages returns a thread's messages in seq order. The thread's existence is not checked here — an unknown thread yields an empty slice; the caller resolves the thread first (GetPlanningThread) when it matters.
func (*Store) ListPlanningThreads ¶ added in v0.8.0
func (s *Store) ListPlanningThreads(ctx context.Context, campaignID uuid.UUID) ([]PlanningThread, error)
ListPlanningThreads returns the campaign's threads, most recently touched first (an exchange bumps updated_at), id tie-broken for a stable order.
func (*Store) ListPlans ¶ added in v0.2.1
ListPlans returns the full catalog (archived included, flagged), stable-ordered by slug.
func (*Store) ListPlayerMaps ¶ added in v0.5.0
ListPlayerMaps is the player-tier read (ADR-0056): gm_private Maps are excluded in the QUERY, so they are absent from the response rather than merely hidden by the UI.
func (*Store) ListPlayerPins ¶ added in v0.5.0
ListPlayerPins is the player-tier read (ADR-0056): a Pin is excluded IN THE QUERY when it is gm_private OR its Node is. A position that points at a GM secret is itself a leak — knowing "something is here" and what it is called is most of the secret — so the Node's flag propagates, mirroring ADR-0008's rule that gm_private filtering applies to expansion and not only to direct reads.
func (*Store) ListPromotedHighlightsNeedingEnrichment ¶ added in v0.2.1
func (s *Store) ListPromotedHighlightsNeedingEnrichment(ctx context.Context, enrichKind string) ([]HighlightEnrichTarget, error)
ListPromotedHighlightsNeedingEnrichment returns every PROMOTED Highlight with an empty image_key and NO enrich job of the given kind in a live state (pending/running/done) — the promoted Highlights whose image enrichment was never enqueued (a crash between promote-commit and the enqueue) or whose only enqueue was lost (#406). It is the (a) half of the boot reconciliation sweep, mirroring ListSessionsNeedingCandidatePurge. A 'done' job counts as satisfied so an unconfigured/failed-permanent enrichment (the handler returns nil and leaves the row imageless by design) is NOT re-swept every boot; 'dead' is treated as absent so a genuinely dead-lettered enrichment is re-scheduled. A job is matched on its payload's highlight_id. Process-wide, carries no tenant.
func (*Store) ListPromotedHighlightsNeedingSoundEnrichment ¶ added in v0.10.0
func (s *Store) ListPromotedHighlightsNeedingSoundEnrichment(ctx context.Context, enrichKind string) ([]HighlightSoundEnrichTarget, error)
ListPromotedHighlightsNeedingSoundEnrichment returns every PROMOTED Highlight with a requested-but-unlanded sound (sound_kind set, sound_key empty) and NO enrich job of the given kind in a live state matching BOTH the highlight AND the requested sound kind (#312). Matching the payload's kind too is what lets a choice CHANGE re-sweep: a 'done' sting job must not satisfy a later music request. 'done' counts as satisfied (an unconfigured no-op is not re-swept every boot); 'dead' is treated as absent so a dead-lettered generation is re-scheduled once the key is fixed. Process-wide, carries no tenant.
func (*Store) ListProviderConfigs ¶
func (s *Store) ListProviderConfigs(ctx context.Context, tenantID uuid.UUID) ([]ProviderConfig, error)
ListProviderConfigs returns all of a Tenant's Provider Configs, ordered by Component then Provider (deterministic for the Configuration screen, #68). An empty result is not an error.
func (*Store) ListSessionAppearances ¶ added in v0.5.0
func (s *Store) ListSessionAppearances(ctx context.Context, sessionID uuid.UUID) ([]SessionAppearance, error)
ListSessionAppearances returns every appearance recorded in one Voice Session, for the history-flagged bundle export.
func (*Store) ListSessionCandidateClipKeys ¶
func (s *Store) ListSessionCandidateClipKeys(ctx context.Context, voiceSessionID uuid.UUID) ([]string, error)
ListSessionCandidateClipKeys returns the clip_key of every remaining CANDIDATE highlight for a Voice Session — the blob keys the 7-day purge job drops through the seam BEFORE it deletes the rows (blob-first, ADR-0048). Promoted rows are excluded (they are kept). Session-scoped (the purge payload carries no tenant).
func (*Store) ListSessionsNeedingCandidatePurge ¶
func (s *Store) ListSessionsNeedingCandidatePurge(ctx context.Context, purgeKind string) ([]SessionPurgeCandidate, error)
ListSessionsNeedingCandidatePurge returns the ENDED Voice Sessions (id + ended_at) that still hold at least one 'candidate' highlight but have NO purge job of the given kind in a live state (pending/running/done) — the sessions whose 7-day purge was never scheduled because a crash landed between the session ending and the Saver's Finalize enqueue (#308, ADR-0051). It is the input to the boot-time backstop sweep. ended_at is returned so the sweep anchors the horizon at session END (ended_at+7d), not boot time. A job is matched on its payload's voice_session_id (the purge payload's only field); 'dead' jobs are treated as absent so a permanently-failed purge is re-scheduled. Session-scoped, carries no tenant (the sweep is process-wide, ADR-0049).
func (*Store) ListTapeConsent ¶
ListTapeConsent returns the Discord user ids that have consented to the rollover tape for a Campaign (#306), the set the tape is seeded with when a Voice Session arms it. Order is stable by created_at for deterministic reads.
func (*Store) ListTenantOperatorBindings ¶ added in v0.4.0
func (s *Store) ListTenantOperatorBindings(ctx context.Context) ([]TenantOperatorBinding, error)
ListTenantOperatorBindings returns each Tenant paired with its operator's Discord snowflake (tenant.operator_user_id → users.discord_user_id) — the per-Tenant GM-identity source (#490, ADR-0055). It is the tenant-scoped analogue of ListTenantOperatorDiscordIDs and shares its exclusions: the synthetic dev operator (DevOperatorDiscordID) never appears (it can match no real speaker/interaction user) and a SUSPENDED operator drops out (open-mode revocation reaches GM on the next snapshot refresh). Sorted for determinism. An empty result is not an error.
func (*Store) ListTenantOperatorDiscordIDs ¶ added in v0.3.0
ListTenantOperatorDiscordIDs returns the DISTINCT Discord snowflakes bound as tenant operators (tenant.operator_user_id → users.discord_user_id), sorted for determinism — the interim GM-identity source (ADR-0055, amending ADR-0050's allowlist-membership clause). auth.GMIdentity snapshots it; unbound tenants and unbound users simply don't appear (the env-allowlist fallback covers the NULL-binding migration edge). The synthetic dev operator (DevOperatorDiscordID) is excluded: it can never match a real Discord speaker or interaction user, so on a dev-touched DB it would only make the GM set look non-empty — suppressing the "Butler unaddressable" boot warning — while admitting nobody. An empty result is not an error.
func (*Store) ListTenantsWithPlan ¶ added in v0.2.1
func (s *Store) ListTenantsWithPlan(ctx context.Context) ([]TenantPlanRow, error)
ListTenantsWithPlan lists every tenant with its active plan slug (empty when unsubscribed) — the operator's `billing tenants` view for finding tenant ids.
func (*Store) ListToolGrants ¶
ListToolGrants returns an Agent's VOICE Tool Grants ordered by tool_name (a stable order so a hydrated GrantSet — and thus the ADR-0021 prompt_hash — does not thrash between runs). An Agent with no grants yields an empty slice, not an error: least-privilege means such an Agent is shown no Tool at all. Voice-only is the pre-0062 meaning every existing caller (wirenpc, the grant editor RPC, the bundle) relies on; the chat surface reads its disjoint rows via Store.ListToolGrantsFor.
func (*Store) ListToolGrantsFor ¶ added in v0.8.0
func (s *Store) ListToolGrantsFor(ctx context.Context, agentID uuid.UUID, surface GrantSurface) ([]ToolGrant, error)
ListToolGrantsFor returns an Agent's Tool Grants for one surface (#592, ADR-0062), ordered by tool_name. The surfaces are disjoint row sets, so a chat hydration can never arm a voice-granted tool and vice versa.
func (*Store) ListTranscriptChunks ¶
func (s *Store) ListTranscriptChunks(ctx context.Context, campaignID uuid.UUID, includeVectors bool) ([]ExportChunk, error)
ListTranscriptChunks returns every Transcript Chunk in a Campaign ordered (created_at, id) — the deterministic read the Campaign Bundle exporter serialises (#288, ADR-0053). It is Campaign-scoped (#342): only matching campaign_id rows are returned, so no Chunk leaks across Campaigns. embedding_model is always selected; the vector is included as its ::text form only when includeVectors is true, else "" (COALESCE handles NULL either way). The exporter passes false (ADR-0053 d3 strips vectors); the flag exists for the backup/migration path. Vectors stay text-form so storage keeps no pgvector-go dependency.
func (*Store) ListTranscriptLines ¶
func (s *Store) ListTranscriptLines(ctx context.Context, sessionID uuid.UUID) ([]TranscriptLine, error)
ListTranscriptLines returns a Voice Session's transcript Lines ordered by seq — the replay-on-reload history the Session screen renders for an ended session (#74). An empty result is not an error.
func (*Store) ListUnembeddedChunks ¶
ListUnembeddedChunks returns up to limit Transcript Chunks still awaiting an embedding (embedding IS NULL), oldest first — the backfill worker's work queue (#116, ADR-0011). Ordering by (created_at, id) makes each pass drain the oldest backlog first and be a stable, deterministic batch. An empty result is not an error: it means the backlog is drained and the worker sleeps.
func (*Store) ListUnembeddedNodes ¶ added in v0.2.1
ListUnembeddedNodes returns up to limit Knowledge Graph Nodes still awaiting an embedding (embedding IS NULL), oldest first — the node half of the embedworker backfill queue (#300, ADR-0011), mirroring ListUnembeddedChunks. An empty result means the backlog is drained. The name + aspects + body is embedded by the worker, so the projection carries the Aspects — private ones included: the embedding feeds the GM-facing similarity hints (ADR-0052), which are never prompt-facing, and a secret that is invisible to the vector would make duplicate detection blind to exactly the facts GMs most want deduped.
func (*Store) ListVoiceSessions ¶
func (s *Store) ListVoiceSessions(ctx context.Context, campaignID uuid.UUID, limit int) ([]VoiceSession, error)
ListVoiceSessions returns a Campaign's Voice Sessions newest-first (started_at DESC, id DESC — the same tiebreak as GetLatestVoiceSession), the running row included, capped at limit. It backs the Session screen's past-session picker (#270): the operator picks a prior session to replay its persisted transcript. It reuses voiceSessionColumns/scanVoiceSession and is served by voice_sessions_campaign_idx (no migration). An empty result is not an error (the never-run picker state).
func (*Store) LoadAgent ¶
LoadAgent loads an Agent together with its bound LLM and TTS Provider Configs — the Persona/Voice/provider bundle the orchestrator needs (the core read this task exists to enable). Missing or unbound configs yield nil, not an error; only the Agent itself must exist.
func (*Store) MapAncestors ¶ added in v0.5.0
MapAncestors returns a Map's ancestor chain, nearest parent first, for the breadcrumb. It is bounded: a cycle (which the schema permits, since a self-reference is only blocked at depth 1) terminates at maxMapDepth rather than looping, so a mis-parented Map degrades to a truncated breadcrumb instead of hanging the screen.
func (*Store) MapSeedContext ¶ added in v0.5.0
func (s *Store) MapSeedContext(ctx context.Context, campaignID, nodeID uuid.UUID) (KGNode, []string, error)
MapSeedContext returns the PUBLIC material a generated map's prompt may be seeded from (#541): the anchor Location itself, and the names of what resides in it.
Public-only, filtered in the QUERY, on the same seam-not-call-site principle PromptKG follows — and for a sharper reason than usual. A generated map is an artefact the GM shows the table. Seeding its prompt from gm_private prose, or from the name of a gm_private neighbour, launders a secret into a picture, and unlike a prompt a picture cannot be filtered afterwards or un-seen. Composing this from ListNodes + NodeEdges would have done exactly that: both are GM-facing reads that deliberately include private rows.
A gm_private anchor yields ErrNotFound. That is not an oversight: a secret place has no public depiction to generate.
func (*Store) MarkJobDead ¶
func (s *Store) MarkJobDead(ctx context.Context, id uuid.UUID, attempts int, lastError string) error
MarkJobDead moves the caller's claimed generation of a job to the dead-letter state with a recorded last_error and a cleared lease — visible, never silently retried again. attempts is the claim generation. A missing id OR a superseded claim yields ErrNotFound.
func (*Store) MarkVoiceSessionIntentLive ¶ added in v0.4.0
func (s *Store) MarkVoiceSessionIntentLive(ctx context.Context, id uuid.UUID, instanceID string, voiceSessionID uuid.UUID) (VoiceSessionIntent, error)
MarkVoiceSessionIntentLive flips instanceID's claimed intent to 'live' and binds the voice_sessions row the worker created, stamping a fresh heartbeat. It is fenced by (id, instance_id, status='claimed'): a superseded caller (a reaper already marked it dead, or a different instance owns it) matches no row and yields ErrNotFound.
func (*Store) NodeEdges ¶
func (s *Store) NodeEdges(ctx context.Context, campaignID, nodeID uuid.UUID) (outgoing, incoming []KGEdgeWithNodes, err error)
NodeEdges returns a Node's incident Edges split by direction — outgoing (from_node_id = nodeID) and incoming (to_node_id = nodeID) — each joined to both endpoints' name/type so the Campaign screen renders without an N+1. One query fetches both directions (ordered created_at, id); the split is done in Go.
The read is scoped to the owning Campaign (#356): the anchor Node must belong to campaignID, else ErrNotFound — a Node in another Campaign is invisible, leaking neither its edges nor its joined endpoint names (incl. gm_private ones) nor an existence oracle. A truly-missing Node id is the same ErrNotFound. Same no-oracle discipline as the cross-campaign DeleteEdge/SetNodeAgent refusals.
func (*Store) NodePins ¶ added in v0.5.0
NodePins returns every Pin for one Node across all Maps in the Campaign — "where is this?", answered from the entry rather than from a Map. It is what makes a Node's position discoverable without opening every Map in turn.
GM-facing: gm_private Pins, Pins on gm_private Maps and Pins on gm_private Nodes are ALL included. Anything prompt- or player-facing must use Store.PlayerNodePins — the two are separate functions rather than one with a flag the caller might forget, because forgetting here means a Character NPC telling the table where the GM's secret is.
func (*Store) NodePortrait ¶ added in v0.8.0
func (s *Store) NodePortrait(ctx context.Context, campaignID, nodeID uuid.UUID) (string, time.Time, error)
NodePortrait returns a Node's portrait blob key (” when it has none) and its updated_at — exactly what the plain-HTTP portrait serve needs (#590): the key to fetch and the cache validator to serve with. Campaign-scoped; a Node outside the campaign is ErrNotFound.
func (*Store) PinsNear ¶ added in v0.5.0
func (s *Store) PinsNear(ctx context.Context, campaignID, mapID uuid.UUID, x, y, radius float64, publicOnly bool, limit int) ([]MapPin, error)
PinsNear returns the Pins within `radius` (in normalized map units) of a point on a Map, nearest first, excluding any Pin at the exact origin — the spatial Tool's "what is around us" read (#539).
publicOnly applies the same composed visibility as Store.ListPlayerPins: a Pin is excluded when it or its Node is gm_private. Every PROMPT-facing caller passes true, because a spatial answer that names a GM secret leaks it just as surely as a fact would.
Distance is Euclidean in normalized space, so it is an aspect-ratio-agnostic approximation of real proximity — good enough for "what is near us", and honest about not being a survey.
func (*Store) PlayerNodePins ¶ added in v0.5.0
PlayerNodePins is Store.NodePins with the privacy filter pushed into SQL: a gm_private Pin, a Pin whose Node is gm_private, and a Pin on a gm_private Map are all invisible. It is the read the prompt-facing spatial Tools consult (#539), so a GM secret cannot reach an NPC's answer even by mistake.
func (*Store) PortraitSeedContext ¶ added in v0.8.0
func (s *Store) PortraitSeedContext(ctx context.Context, campaignID, nodeID uuid.UUID) (KGNode, error)
PortraitSeedContext returns the PUBLIC material a generated portrait's prompt may be seeded from (#590): the Node itself with its public Aspects only.
Public-only, filtered in the QUERY, on the same seam-not-call-site principle MapSeedContext follows, and for the same reason: a portrait is an artefact the GM shows the table, so seeding its prompt from gm_private prose or facts launders a secret into a picture that cannot be filtered afterwards.
A gm_private Node yields ErrNotFound — a secret character has no public depiction to generate. The GM can still upload a portrait for one by hand.
func (*Store) PromoteHighlight ¶
PromoteHighlight flips a candidate to 'promoted' and stamps promoted_at within the tenant, returning the updated row (#309 GM keep). It is idempotent: a re-promote keeps the ORIGINAL promoted_at (COALESCE) so the audit trail of WHEN the GM first kept it survives. A missing id (or foreign tenant) yields ErrNotFound. It deliberately does NOT enqueue enrichment — that is #311's hook.
func (*Store) PromptKG ¶ added in v0.2.1
func (s *Store) PromptKG() PromptKGView
PromptKG returns the prompt-facing read view of this Store — the ONLY KG handle prompt-assembly wiring should be given (#450).
func (*Store) ProvisionSignup ¶ added in v0.3.0
func (s *Store) ProvisionSignup(ctx context.Context, p SignupParams) (SignupResult, error)
ProvisionSignup admits an open-mode signup in ONE transaction: Discord user upsert → bound-tenant lookup → (when none) create-only Tenant founding + default-Plan bind → session mint. All-or-nothing per ADR-0055: a failure at any step (most plausibly an unknown or archived default plan slug) leaves no user, tenant, subscription, or session behind. A returning signup — the user already bound to a Tenant — founds nothing and just refreshes identity + mints the session. A suspended user is refused with ErrUserSuspended.
func (*Store) ReapDeadVoiceSessionIntents ¶ added in v0.4.0
func (s *Store) ReapDeadVoiceSessionIntents(ctx context.Context, expiry time.Duration) (int64, error)
ReapDeadVoiceSessionIntents marks 'dead' every claimed/live intent whose heartbeat is older than expiry — the owning Voice Instance is presumed crashed (ADR-0006/0057 (e): no takeover, so a stale claim is a death, not a hand-off). The Tenant sees the dead state and can restart. A fresh heartbeat (within expiry) is untouched. Returns how many rows were reaped. Called once per claim loop tick before claiming, mirroring SweepExpiredJobs (ADR-0049).
func (*Store) ReapVoiceSessionIntentIfExpired ¶ added in v0.4.0
func (s *Store) ReapVoiceSessionIntentIfExpired(ctx context.Context, id uuid.UUID, expiry time.Duration) (bool, error)
ReapVoiceSessionIntentIfExpired marks ONE claimed/live intent dead when its heartbeat is older than expiry — the zero-worker escape (#491 review item 4): the reaper otherwise runs only inside a worker's claim tick, so with NO healthy worker a dead claimed/live intent would never expire and its Tenant would stay blocked (ErrIntentActive) forever. IntentControl.Start calls this on the exact row blocking a retry. Fenced by id + a stale heartbeat, so a live row (fresh beat) or a foreign status is untouched. Returns whether it reaped a row.
func (*Store) ReconcileOrphanedVoiceSessions ¶
ReconcileOrphanedVoiceSessions closes every Voice Session row still marked 'running' — at startup no live loop exists, so any such row is an orphan from a crash or a failed end-write (#143). Each is stamped ended_at=now(), status='ended' and the distinguishing VoiceSessionReasonOrphaned end_reason (a clean end leaves end_reason NULL). Returns how many rows were closed. Called by the SessionManager at boot, before any session can start.
func (*Store) ReconcileWorkerOrphanedVoiceSessions ¶ added in v0.4.0
ReconcileWorkerOrphanedVoiceSessions closes 'running' voice_sessions rows that a -mode voice worker left behind on a crash — NEVER a row a live worker still owns (#491, the reviewer-flagged process-blindness): a plain ReconcileOrphanedVoiceSessions is process-blind, so two workers booting would close each other's live 'running' rows. Two worker-safe arms:
- Rows BOUND to a now-terminal intent (dead/done/failed) — the ordinary crashed-worker leftovers.
- Rows whose Campaign has NO non-terminal intent at all (#483 M2): a worker dying between CreateVoiceSession and MarkVoiceSessionIntentLive leaves a 'running' row its intent never bound (voice_session_id NULL), so the reaped 'dead' intent can never match arm 1 and the row would sit 'running' forever. No non-terminal intent for the Campaign means no worker can be mid-Start or live on it (the gap itself holds a 'claimed' intent, and a live session holds a 'live' one), so closing is safe. An unbound orphan whose Tenant has ALREADY restarted (a fresh non-terminal intent on the same Campaign) is deferred until that intent goes terminal — eventual, never wrong.
Returns how many rows were closed. The -mode all path keeps the broad [ReconcileOrphanedVoiceSessions] (it writes no intents; mixing the two shapes on one DB is refused at boot, see cmd/glyphoxa's mixed-mode guard).
func (*Store) RecordAdmissionPosture ¶ added in v0.3.0
RecordAdmissionPosture upserts the singleton deployment_settings row with the effective Admission Mode, so the posture survives env-var loss and stays visible to operators (ADR-0055's rollback-trap mitigation).
func (*Store) RecordNodeAppearances ¶ added in v0.5.0
func (s *Store) RecordNodeAppearances(ctx context.Context, rows []NodeAppearance) error
RecordNodeAppearances writes a batch of appearances, ignoring ones already recorded.
ON CONFLICT DO NOTHING is what makes the indexing job safely re-runnable: a retried or replayed job re-derives the same (node, line) pairs and writes nothing new, so nobody has to reason about whether it already ran.
func (*Store) RejectKnowledgeProposal ¶ added in v0.2.1
RejectKnowledgeProposal drops a pending proposal (status rejected, reviewed_at stamped) WITHOUT touching the KG (#300, ADR-0052). The row is kept for audit. A missing/already-reviewed id — or one in another Campaign — yields ErrNotFound.
func (*Store) ReleaseDiscordGuild ¶ added in v0.4.0
func (s *Store) ReleaseDiscordGuild(ctx context.Context, tenantID uuid.UUID, guildID string) (DeploymentConfig, error)
ReleaseDiscordGuild frees a Tenant's bound guild (#504): an atomic compare-and-clear — the caller echoes the guild_id it believes is bound, and the row is cleared only when tenant AND guild match (no read-then-write race). guild_id = ” is the unconfigured state (migration 00037), so release needs no schema change and frees the first-registrar-wins index slot for the next binder (legit transfer: A releases, B saves with proof). The Bot token is untouched. No matching bound row — wrong echo, already released, or no config at all — returns ErrNotFound.
func (*Store) ReleaseHighlightEnrichClaim ¶ added in v0.2.1
ReleaseHighlightEnrichClaim clears a Highlight's enrichment claim (#406) so a retry (or a later re-promotion) can re-claim without waiting out the ttl. It is idempotent — clearing an already-null claim is a no-op — and tenant-free.
func (*Store) ReleaseHighlightSoundEnrichClaim ¶ added in v0.10.0
ReleaseHighlightSoundEnrichClaim clears a Highlight's sound-generation claim (#312) so a retry (or a re-run of the Add-sound action) can re-claim without waiting out the ttl. Idempotent and tenant-free.
func (*Store) ReleasePresenceOwner ¶ added in v0.4.0
ReleasePresenceOwner drops instanceID's presence-owner claim so a challenger's very next AcquireOrRenewPresenceOwner wins immediately (a clean drain handover, not an expiry wait). Fenced by instance_id: a superseded former owner that already lost the row deletes nothing, never the new owner's claim. Deleting no row is not an error.
func (*Store) RenameAgentNode ¶ added in v0.3.0
func (s *Store) RenameAgentNode(ctx context.Context, campaignID, agentID uuid.UUID, oldName, newName string) error
RenameAgentNode renames an Agent's linked NPC Node WHEN the two names still match (#479, ADR-0008 second amendment): after the auto-created Node, a GM renaming the Agent (typically off the "New NPC" placeholder) keeps the wiki entry in step — but once the GM has renamed the Node independently, the names diverge and the follow stops for good; bodies are never synced. The rename resets the embedding like any other name edit (ADR-0011) so similarity hints re-embed the new text. Matching no row (no linked Node, names already diverged, or oldName == newName) is a successful no-op.
func (*Store) RenameBoard ¶ added in v0.5.0
RenameBoard renames a prep board.
func (*Store) RenamePlanningThread ¶ added in v0.8.0
func (s *Store) RenamePlanningThread(ctx context.Context, campaignID, id uuid.UUID, title string) (PlanningThread, error)
RenamePlanningThread sets a thread's title and returns the updated row. A thread outside the campaign yields ErrNotFound.
func (*Store) RenameTag ¶ added in v0.5.0
RenameTag renames a tag across the whole Campaign — the AC's "renaming a tag updates every use". Done in SQL rather than per-node, because a tag is one concept and renaming it entry-by-entry is how half-renamed vocabularies happen.
A node already carrying the target name keeps a single row: the ON CONFLICT drops the duplicate rather than failing the whole rename.
func (*Store) RenameTenant ¶ added in v0.3.0
RenameTenant sets the Tenant's display name — the ADR-0055 onboarding step ("name your Tenant") for a freshly signed-up founder. Validation (non-empty, 200-character cap) lives at the RPC (AuthServer.RenameTenant); storage stays faithful. ErrNotFound for an unknown tenant.
func (*Store) ReplaceNodeAspects ¶ added in v0.5.0
func (s *Store) ReplaceNodeAspects(ctx context.Context, campaignID, nodeID uuid.UUID, w KGNodeAspectWrite) error
ReplaceNodeAspects rewrites a Node's Aspects to the supplied list, preserving rows the caller had not loaded and keeping row identity stable (see KGNodeAspectWrite), inside one transaction.
The write is scoped to (node_id, campaign_id) like every other KG mutation (#342): a Node in another Campaign matches nothing, so nothing is removed and the insert is refused by the composite FK.
An empty Rows list clears the Aspects the caller knew about (the Node keeps its free-form body). Exceeding kgvocab.MaxAspectsPerNode once survivors are counted yields ErrAspectsFull.
func (*Store) RequestVoiceSessionStop ¶ added in v0.4.0
func (s *Store) RequestVoiceSessionStop(ctx context.Context, id uuid.UUID) (VoiceSessionIntent, error)
RequestVoiceSessionStop asks the claim plane to wind an intent down. A still 'pending' intent (no worker has claimed it) is taken straight to 'done' with ended_at set — there is no worker to honor a flag, so the stop resolves immediately. A claimed/live intent instead sets stop_requested=true, which the owning worker sees on its next heartbeat and acts on. A missing id, or an already-terminal intent, yields ErrNotFound. Returns the updated row.
func (*Store) ResolveMapByName ¶ added in v0.5.0
func (s *Store) ResolveMapByName(ctx context.Context, campaignID uuid.UUID, name string) (CampaignMap, error)
ResolveMapByName finds a Map in a Campaign by case-insensitive name — what the /where slash command resolves its argument through. Exactly one match is required: zero or many are refused so the GM fixes the ambiguity rather than the party being silently teleported to whichever row sorted first.
func (*Store) ResolveOperatorTenant ¶
ResolveOperatorTenant binds the single seeded Tenant to the first operator and returns it (ADR-0039). It is idempotent and atomic: if a tenant is already bound to the user it is returned; otherwise the earliest claimable tenant — unbound (the seed's) or held by the synthetic dev operator (ADR-0041 GLYPHOXA_DEV_MODE, see DevOperatorDiscordID) — is claimed; otherwise — a fresh DB with no seed — a new 'Glyphoxa' tenant is created bound to the user. Called once on OAuth login and by the dev-mode boot.
func (*Store) ResolvePinByLabel ¶ added in v0.5.0
func (s *Store) ResolvePinByLabel(ctx context.Context, campaignID, mapID uuid.UUID, label string) (MapPin, error)
ResolvePinByLabel finds a Pin on a Map by its displayed label (its override, or its Node's name), case-insensitively. Same one-match rule as ResolveMapByName, for the same reason.
func (*Store) RetryJob ¶
func (s *Store) RetryJob(ctx context.Context, id uuid.UUID, attempts int, lastError string, runAfter time.Time) error
RetryJob returns the caller's claimed generation of a job to pending with a recorded last_error and a future run_after (the backoff delay), clearing its lease so it is re-claimable once run_after arrives. attempts is the claim generation. A missing id OR a superseded claim yields ErrNotFound.
func (*Store) RevokeSessionsOutsideAllowlist ¶
func (s *Store) RevokeSessionsOutsideAllowlist(ctx context.Context, discordUserIDs []string) (int64, error)
RevokeSessionsOutsideAllowlist deletes every session whose owning user's discord_user_id is not on the operator allowlist (ADR-0041 amendment, issue #184). The allowlist gates only NEW logins at the OAuth callback; this sweep is the revocation half, run at every non-dev ALLOWLIST-Admission-Mode web/all boot — which is exactly when a grant change takes effect, since the env var is parsed at boot. The sweep splits by Admission Mode (ADR-0055): an `open` boot must never run it (it would log out every signup each restart — revocation there is suspension-based), and flipping open -> allowlist plus a restart evicts every signup, the deliberate lock-down. It also clears leftover GLYPHOXA_DEV_MODE sessions (DevOperatorDiscordID is never allowlisted). An empty allowlist is refused defensively: it would revoke every session, and in allowlist mode the boot preflight guarantees a non-empty list at the only call site (sweepAllowlistSessions).
func (*Store) SaveDefaultVoiceChannel ¶ added in v0.6.0
func (s *Store) SaveDefaultVoiceChannel(ctx context.Context, tenantID uuid.UUID, voiceChannelID string) (DeploymentConfig, error)
SaveDefaultVoiceChannel stores the Tenant's Default Voice Channel — the channel a session start falls back to when no explicit pick rides the StartSession request. It requires a linked guild (a default channel is meaningless without one, and the picker that offers channels needs the guild anyway): no bound-guild row yields ErrNotFound, which the RPC maps to a "link a Discord server first" precondition.
func (*Store) SaveDiscordBotToken ¶
func (s *Store) SaveDiscordBotToken(ctx context.Context, tenantID uuid.UUID, ciphertext []byte, last4 string) (DeploymentConfig, error)
SaveDiscordBotToken upserts only the deployment Bot token columns (sealed ciphertext + last4), leaving the Guild / Voice channel IDs untouched. It returns the resulting row. The ciphertext is the caller-sealed secret.
func (*Store) SaveDiscordChannels ¶
func (s *Store) SaveDiscordChannels(ctx context.Context, tenantID uuid.UUID, guildID, voiceChannelID string) (DeploymentConfig, error)
SaveDiscordChannels upserts only the non-secret Guild / Voice channel IDs, leaving the Bot token untouched, and returns the resulting row. A guild_id already bound by a DIFFERENT Tenant trips the first-registrar-wins partial UNIQUE index (23505 on deployment_config_guild_owner) and yields ErrGuildTaken (#483/#504) — the same Tenant re-saving its own guild upserts its own row and never conflicts.
func (*Store) SaveDiscordGuild ¶ added in v0.6.0
func (s *Store) SaveDiscordGuild(ctx context.Context, tenantID uuid.UUID, guildID string) (DeploymentConfig, error)
SaveDiscordGuild upserts only the guild_id, leaving the Bot token untouched, and returns the resulting row. The stored Default Voice Channel (voice_channel_id) survives a re-save of the SAME guild but is cleared when the guild changes — a channel snowflake belongs to one guild, so carrying it across a re-bind would point sessions at a channel of the OLD guild. A guild_id already bound by a DIFFERENT Tenant trips the first-registrar-wins partial UNIQUE index (23505 on deployment_config_guild_owner) and yields ErrGuildTaken, exactly like SaveDiscordChannels.
func (*Store) SearchChunksByAgent ¶
func (s *Store) SearchChunksByAgent(ctx context.Context, campaignID, agentID uuid.UUID, query []float32, k int) ([]ChunkMatch, error)
SearchChunksByAgent is NPC-knowledge retrieval (ADR-0011): the k Transcript Chunks in campaignID nearest the query vector by cosine distance whose participated set CONTAINS agentID — "what this NPC could personally know". A multi-agent chunk is returned for every one of its participants (containment, not equality). NULL-embedding rows are excluded (partial HNSW index).
func (*Store) SearchChunksByCampaign ¶
func (s *Store) SearchChunksByCampaign(ctx context.Context, campaignID uuid.UUID, query []float32, k int) ([]ChunkMatch, error)
SearchChunksByCampaign is world-context retrieval (ADR-0011): the k Transcript Chunks in campaignID nearest the query vector by cosine distance, participants ignored — topical Campaign context the NPC "may not personally know". NULL-embedding rows are excluded (partial HNSW index).
func (*Store) SearchNodes ¶
func (s *Store) SearchNodes(ctx context.Context, campaignID uuid.UUID, query string, limit int) ([]KGNode, error)
SearchNodes returns the Campaign's Knowledge Graph Nodes whose name or body match the query, ranked by relevance (ts_rank over the weighted fts column: name weight A outranks body weight B), then newest-first, then id. The search is served by the kg_node_fts_idx GIN index (fts @@ q), not a substring scan. gm_private Nodes are INCLUDED (GM-facing search). An empty BuildTSQuery result yields (nil, nil) — no matches, not an error.
func (*Store) SearchPromotedHighlights ¶ added in v0.8.0
func (s *Store) SearchPromotedHighlights(ctx context.Context, tenantID, campaignID uuid.UUID, query string, limit int) ([]Highlight, error)
SearchPromotedHighlights returns the Campaign's PROMOTED Highlights whose excerpt or reason matches the query, ranked by relevance (ts_rank over the weighted fts column: excerpt weight A outranks reason weight B), then newest moment first, then id. Tenant- AND campaign-scoped in the query, like every Highlight read. An empty BuildTSQuery result yields (nil, nil) — no matches, not an error.
func (*Store) SearchPublicNodes ¶
func (s *Store) SearchPublicNodes(ctx context.Context, campaignID uuid.UUID, query string, limit int) ([]KGNode, error)
SearchPublicNodes is SearchNodes with gm_private Nodes EXCLUDED IN THE QUERY — the prompt-facing knowledge search (#296). The exclusion is pushed into the WHERE clause so it applies BEFORE the LIMIT: a post-fetch filter in Go would drop the top-N ranked hits if they were all gm_private and starve a public match ranked N+1. This is the load-bearing ADR-0008 guard for the kg_query Tool — a GM-only Node must never reach an NPC's prompt. Existing GM-facing callers keep SearchNodes (the wiki search shows private Nodes to the GM).
func (*Store) SearchTranscriptLines ¶
func (s *Store) SearchTranscriptLines(ctx context.Context, campaignID uuid.UUID, query string, limit int) ([]TranscriptLine, error)
SearchTranscriptLines returns a Campaign's persisted transcript Lines whose text matches the query, ranked by relevance (#120, ADR-0011 amendment). This is the SINGLE user-facing search path — both the web SearchTranscriptLines RPC and the `/glyphoxa search` slash command call exactly this method (AC4: no divergent search logic). The match is served by the transcript_line_fts_idx GIN index (fts @@ q), not a substring scan, and ranked with ts_rank (a line that mentions the term more often ranks higher), then newest-first, then a stable tiebreak. The query is scoped by campaign_id, so another Campaign's transcript is NEVER returned (AC5). The raw query is sanitized by BuildTSQuery (the same helper the KG search uses), so injected tsquery operators can only ever split words; an empty result from that yields (nil, nil) — no matches, not an error.
func (*Store) SessionLinesForIndexing ¶ added in v0.5.0
func (s *Store) SessionLinesForIndexing(ctx context.Context, sessionID uuid.UUID) ([]IndexableLine, error)
SessionLinesForIndexing returns a session's committed Transcript Lines.
Committed only, because that is all transcript_line ever holds: a partial never reaches it (ADR-0012/ADR-0040), so "appearances come from committed lines" needs no filter here — it is a property of the table.
func (*Store) SetActiveCampaign ¶
func (s *Store) SetActiveCampaign(ctx context.Context, discordUserID string, campaignID uuid.UUID) error
SetActiveCampaign records the operator's durable Active Campaign choice (#108, ADR-0009) on the users row keyed by Discord snowflake. It UPSERTS the row so a GM who drives the slash-command surface before ever logging into the web tier still gets a persisted selection; a later OAuth login refreshes the display fields (UpsertUser) and preserves this selection. Idempotent. A campaignID that no longer exists trips the active_campaign_id FK (23503) and yields ErrNotFound — the write can never store a dangling pointer, even if the campaign vanishes between a caller's existence check and this write.
func (*Store) SetBoardNodes ¶ added in v0.5.0
func (s *Store) SetBoardNodes(ctx context.Context, campaignID, boardID uuid.UUID, nodeIDs []uuid.UUID) error
SetBoardNodes replaces a board's entries with exactly the supplied list, in order. Replace-in-full for the same reason the aspect editor uses it: add, remove and reorder are one save, and positions stay dense by construction.
func (*Store) SetCampaignShareChannel ¶ added in v0.2.1
func (s *Store) SetCampaignShareChannel(ctx context.Context, campaignID uuid.UUID, channelID string) error
SetCampaignShareChannel remembers channelID as the Campaign's highlight-share text channel (last-choice-wins). An unknown campaign is ErrNotFound. Persisting it is best-effort from the RPC's view — a failure never fails the share itself, only the pre-selection memory.
func (*Store) SetChunkEmbedding ¶
func (s *Store) SetChunkEmbedding(ctx context.Context, id uuid.UUID, vec []float32, model string) error
SetChunkEmbedding fills one chunk's embedding vector and stamps the model that produced it (#116, ADR-0011). The vector is passed as pgvector's text form and cast server-side (::vector) so storage carries no pgvector-go dependency; the column is vector(768), so a wrong-length vector is rejected by Postgres. Once set, the row leaves the NULL-embedding backlog and becomes returnable by the embedding-filtered retrieval query (embedding IS NOT NULL).
func (*Store) SetHighlightImage ¶ added in v0.2.1
func (s *Store) SetHighlightImage(ctx context.Context, id uuid.UUID, imageKey, contentType string, sizeBytes int64) error
SetHighlightImage lands an AI-generated image on a Highlight (#311): the enrichment job (ADR-0049) stores the image behind the blob seam (ADR-0048) and then records its key, MIME type, and size here. It is tenant-free (the handler carries no tenant — the id scopes the row, and image_key derives from the tenant baked into the blob key) and returns ErrNotFound if the row is gone (a Highlight deleted between the job's GetHighlight and this write — the handler compensates by deleting the just-stored blob). Idempotent at the row level: a re-run overwrites the same deterministic key with the same fields.
func (*Store) SetHighlightSound ¶ added in v0.10.0
func (s *Store) SetHighlightSound(ctx context.Context, id uuid.UUID, kind, soundKey, contentType string, sizeBytes int64) error
SetHighlightSound lands a generated sound asset on a Highlight (#312): the enrichment job stores the audio behind the blob seam (ADR-0048) and then records its key, MIME type, and size here. Tenant-free like SetHighlightImage (the id scopes the row). The write is CONDITIONAL on sound_kind still equalling the kind the job generated for: the GM can change or clear the choice while a generation is in flight, and the stale job's asset must not land under the newer choice. ErrNotFound covers both misses — row gone and kind superseded — and the handler disambiguates with a re-read (only a GONE row compensates the blob; a superseded kind leaves it for the newer job to overwrite).
func (*Store) SetHighlightSoundKind ¶ added in v0.10.0
func (s *Store) SetHighlightSoundKind(ctx context.Context, tenantID, id uuid.UUID, kind string) (Highlight, error)
SetHighlightSoundKind records the GM's "Add sound" choice on a PROMOTED Highlight within the tenant (#312), returning the updated row. A non-empty kind ("sting"/"music" — the RPC validates) stamps sound_requested_at and clears any previously landed sound triad, so the row reads "requested but not landed" until the generation job attaches the new asset; kind "" clears the choice entirely (None). The claim column is deliberately untouched: a live generation keeps serializing blob writes, and the superseded job releases its own claim when its conditional land misses (see SetHighlightSound). A missing/foreign id — or a still-candidate row (sound is opt-in AFTER promotion, #312) — yields ErrNotFound.
func (*Store) SetMapImage ¶ added in v0.5.0
func (s *Store) SetMapImage(ctx context.Context, campaignID, id uuid.UUID, blobKey string, widthPx, heightPx int) (CampaignMap, error)
SetMapImage repoints a Map at new image bytes (#538). Pins are untouched by design: coordinates are normalized, so a re-upload at a different resolution keeps every pin exactly where the GM put it — which is the whole reason they are normalized.
func (*Store) SetNodeAgent ¶
func (s *Store) SetNodeAgent(ctx context.Context, campaignID, nodeID uuid.UUID, agentID uuid.NullUUID) (KGNode, error)
SetNodeAgent links or unlinks a Node's Character NPC Agent (the "voiced by" link, ADR-0008 amendment). Both paths are scoped to the owning Campaign (#342): every UPDATE matches the Node's campaign_id against the caller's campaignID, so a Node in another Campaign is invisible and yields ErrNotFound — a cross-campaign link or unlink is refused server-side. A valid agentID links: the single UPDATE also matches the Node's campaign against the Agent's in one statement, so a missing or cross-campaign Agent matches no row and yields ErrNotFound; a non-NPC Node trips the DB CHECK (ErrInvalidEdge); an Agent already linked to another Node trips the UNIQUE index (ErrConflict). An invalid agentID unlinks (agent_id = NULL). A missing Node yields ErrNotFound.
func (*Store) SetNodeEmbedding ¶ added in v0.2.1
func (s *Store) SetNodeEmbedding(ctx context.Context, id uuid.UUID, vec []float32, model string, updatedAt time.Time) error
SetNodeEmbedding fills one Node's embedding vector and stamps the model that produced it (#300, ADR-0011), mirroring SetChunkEmbedding. The vector is passed as pgvector's text form and cast server-side (::vector) so storage carries no pgvector-go dependency; the column is vector(768), so a wrong-length vector is rejected by Postgres. Once set, the row leaves the NULL-embedding backlog and becomes returnable by SimilarNodes.
Unlike a Transcript Chunk, a Node is MUTABLE: a GM edit or a fact-approval can reset the embedding to NULL and bump updated_at WHILE the worker's (up to 60s) Embed call is in flight. Writing the now-stale vector back would install a v1 vector on a v2 row and it would never re-embed (embedding IS NOT NULL). So the write is guarded on the updated_at the worker LISTED: a row edited since is not matched (0 rows) and stays NULL for the next pass to re-embed with fresh text.
func (*Store) SetNodePortrait ¶ added in v0.8.0
func (s *Store) SetNodePortrait(ctx context.Context, campaignID, nodeID uuid.UUID, blobKey string) (KGNode, string, error)
SetNodePortrait repoints a Node at new portrait bytes (” clears it) and stamps updated_at = now(), which is the portrait URL's cache validator — a portrait write always mints a NEW blob key (ReplaceMapImage's ritual), so a stale validator can never serve stale bytes.
It returns the updated Node (Aspects included, GM read) and the PREVIOUS portrait blob key (” when there was none) so the caller can release the superseded bytes through the seam. The write is scoped to (id, campaign_id) (#342); a Node in another Campaign yields ErrNotFound.
The embedding columns are deliberately untouched: a portrait changes no embedded text. The updated_at bump can at worst make an in-flight embedworker write miss its stale-guard and re-embed next pass (#300) — correct, merely a re-run.
func (*Store) SetNodeTags ¶ added in v0.5.0
SetNodeTags replaces a Node's tags with exactly the supplied set, in one transaction. Replace-in-full matches how the chip editor works — add and remove are one save — and there is no ordering to preserve, so no position column.
Tags are created on use: there is no tag table to maintain and no lifecycle to get wrong, which is the point of "free-form".
func (*Store) SetPartyMarker ¶ added in v0.5.0
func (s *Store) SetPartyMarker(ctx context.Context, campaignID, sessionID uuid.UUID, mapID, pinID uuid.NullUUID, x, y *float64) error
SetPartyMarker places (or clears) a Voice Session's marker. A zero mapID clears it entirely; a set mapID with no pin and no coordinates means "somewhere on this map", which is a legitimate answer while the GM is still deciding.
The write is scoped to (id, campaign_id): a session from another Campaign matches nothing and yields ErrNotFound.
The Map and Pin are verified to belong to THIS Campaign, and the Pin to that Map, inside the same statement. voice_sessions has only plain single-column FKs to them, so without these EXISTS guards any existing map/pin uuid would be accepted — a marker pointing into another Tenant's world, whose label the location clause would then read out at the table. The guards are in the SQL, not in the caller, because every future caller inherits them there.
func (*Store) SetTenantPlan ¶ added in v0.2.1
func (s *Store) SetTenantPlan(ctx context.Context, tenantID uuid.UUID, slug string) (Subscription, error)
SetTenantPlan subscribes a Tenant to the plan with slug, snapshotting the plan's current slug + monthly price onto the new subscription row. Any active subscription is ended first (its EndedAt set), so the partial unique index keeps at most one active row per tenant. ErrNotFound for an unknown slug or tenant; ErrPlanArchived for an archived one.
func (*Store) SetTenantSpendCaps ¶
SetTenantSpendCaps writes a Tenant's soft/hard spend caps, storing NULL for a nil pointer (that cap cleared). It does NOT enforce hard >= soft — that validation is the RPC's (InvalidArgument), leaving storage a faithful persistence layer. ErrNotFound when the tenant row does not exist.
func (*Store) SetUserSuspended ¶ added in v0.3.0
SetUserSuspended stamps or clears users.suspended_at by Discord snowflake — the open-mode revocation mechanism (ADR-0055). Suspension is enforced by AuthenticateSession's per-request re-check, so it takes effect immediately without deleting sessions; clearing it restores access with the same tokens. Idempotent per direction; ErrNotFound for an unknown user.
func (*Store) SimilarNodePairs ¶ added in v0.5.0
func (s *Store) SimilarNodePairs(ctx context.Context, campaignID uuid.UUID, minSimilarity float64, limit int) ([]KGNodePair, error)
SimilarNodePairs returns the Campaign's closest Node pairs by embedding cosine similarity, closest first (#536, ADR-0011). It is the "probable duplicates" derivation the health panel offers — a HINT the GM acts on, never an auto-merge: ADR-0052 rejected auto-merging near-duplicates because similarity is not a semantic judgment and a wrong merge corrupts canon invisibly. The same reasoning applies here, so this read exists only to point at pairs.
It is an exact pairwise scan rather than an ANN probe: a Campaign is hundreds of Nodes, the read is GM-INITIATED (never on render), and an exact answer beats an approximate one for something a human is about to act on. Rows without an embedding are invisible until the backfill worker reaches them.
minSimilarity is the cosine-similarity floor; limit caps the result.
Cost: the pairwise join cannot use the HNSW index — it is a nested loop, O(n²) distance evaluations. At campaign scale (hundreds of Nodes) that is tens of milliseconds, which is why an exact answer is affordable here; it would not be at thousands, and the GM-initiated framing is what keeps that bounded.
func (*Store) SimilarNodes ¶ added in v0.2.1
func (s *Store) SimilarNodes(ctx context.Context, campaignID uuid.UUID, query []float32, k int) ([]KGNode, error)
SimilarNodes returns the k Knowledge Graph Nodes in campaignID nearest the query vector by cosine distance (<=>), nearest first — the ADR-0011 similarity hint the GM review surface shows beside a Knowledge Proposal (#300, ADR-0052). NULL-embedding rows are excluded (the partial HNSW index). Unlike the prompt-facing searches this INCLUDES gm_private Nodes: it is GM-facing review only, and it is deliberately NOT part of PromptKGView — prompt assembly cannot reach it (#450). k <= 0 is a caller bug and errors. The query vector reuses encodeVector + a server-side ::vector cast, so storage carries no pgvector-go dependency.
func (*Store) StartVoiceSessionControl ¶ added in v0.4.0
func (s *Store) StartVoiceSessionControl(ctx context.Context, id uuid.UUID) (VoiceSessionControl, error)
StartVoiceSessionControl fences the hosting worker's pending→executing CLAIM before it runs the verb (#503 FIX1): a transient finish-write failure then leaves the row 'executing' (never back to 'pending'), so it can never be re-listed and re-dispatched — say publishes exactly once. Fenced WHERE status='pending': a second claim, a cancelled row, or a swept row matches nothing and yields ErrNotFound (the worker skips it). Stamps started_at for the stale-executing sweep.
func (*Store) SweepExpiredJobs ¶
SweepExpiredJobs dead-letters every running job in kinds whose lease has expired AND whose attempts have hit max_attempts — the leftover a crash leaves that ClaimJob's runnable guard (attempts < max_attempts) will never re-claim. It stamps a last_error where none was recorded. Returns how many rows were swept. Called once per runner poll before the claim loop.
func (*Store) SweepOrphanedVoiceSessionControls ¶ added in v0.4.0
func (s *Store) SweepOrphanedVoiceSessionControls(ctx context.Context, executingStale time.Duration) (int64, error)
SweepOrphanedVoiceSessionControls retires unfinished (pending/executing) control rows the worker will never complete (#503), run each claim-loop tick. Two arms:
- Rows whose intent is already TERMINAL (done/dead/failed) — controls are never dispatched during a wind-down (dispatch is gated on the session being live), so a stopping session's stragglers die here with the ENCODED no_active_session cause (FIX3) so the requester decodes it to ErrNoActiveSession and the GM sees the plain guard.
- 'executing' rows of a still-live intent stranded past executingStale (a finish-write blip with the requester already gone) — the bounded recovery that keeps such a row from sitting forever (FIX1). Its cause is a plain stall message (uncoded, surfaced verbatim to any late poller).
Returns how many rows were failed.
func (*Store) SyncPlans ¶ added in v0.2.1
func (s *Store) SyncPlans(ctx context.Context, specs []PlanSpec, archiveMissing bool) (PlanSyncResult, error)
SyncPlans upserts the catalog specs by slug (reviving a previously archived slug) and, when archiveMissing is set, archives every plan whose slug is absent from specs. Runs in one transaction: a partially applied catalog is never visible. Plans are never deleted — subscriptions reference them.
func (*Store) TenantForUser ¶
TenantForUser returns the id of the tenant bound to the operator, or ErrNotFound when none is bound. The X-Tenant-Id interceptor uses it as the thin single-operator pass-through (ADR-0039).
func (*Store) TenantHasPlatformKeySource ¶ added in v0.3.0
TenantHasPlatformKeySource reports whether an ACTIVE subscription on a key_source='platform' Plan backs the tenant — the ADR-0054 entitlement-seam read behind llmbuild.SubscriptionKeyGate (ADR-0055 gate (a)). A BYOK plan, an ended subscription, no subscription, or an unknown tenant are all plainly false, never an error: absence of entitlement is the expected common case.
func (*Store) TenantIncludedUsageUSD ¶ added in v0.3.0
TenantIncludedUsageUSD returns the monthly usage allowance of the Tenant's ACTIVE subscription's plan, joined LIVE from the plan row (catalog edits to the allowance apply immediately — subscriptions snapshot only slug + price). nil means no gate applies: no active subscription, or a plan with no configured allowance (NULL — every BYOK plan, by catalog validation). This is one of the two reads behind the ADR-0055 monthly allowance gate (b); the gating decision itself lives outside storage.
func (*Store) TenantMonthUsageUSD ¶ added in v0.3.0
func (s *Store) TenantMonthUsageUSD(ctx context.Context, tenantID uuid.UUID, from, to time.Time) (float64, error)
TenantMonthUsageUSD sums the Usage Ledger's estimated USD for the tenant over days in [from, to) — the BillingReport window convention. A tenant with no rows sums to zero. The ledger stays attribution-only (ADR-0054): this is a plain read the allowance gate consumes, not a gate.
func (*Store) TryClaimHighlightEnrich ¶ added in v0.2.1
func (s *Store) TryClaimHighlightEnrich(ctx context.Context, id uuid.UUID, ttl time.Duration) (bool, error)
TryClaimHighlightEnrich atomically claims the image enrichment of a Highlight (#406): a conditional UPDATE that stamps image_enrich_claimed_at iff the row is still imageless AND no claim newer than ttl is held. It reports whether THIS caller won (RowsAffected == 1). A false-no-error means a live worker holds the claim or the row was enriched meanwhile. The lease (ttl) makes a crashed claimant's claim reclaimable, so a Highlight is never stranded imageless. The column is never scanned onto the wire, so the marker cannot leak into an RPC response. Tenant-free (the id scopes the row, like SetHighlightImage).
func (*Store) TryClaimHighlightSoundEnrich ¶ added in v0.10.0
func (s *Store) TryClaimHighlightSoundEnrich(ctx context.Context, id uuid.UUID, ttl time.Duration) (bool, error)
TryClaimHighlightSoundEnrich atomically claims the sound generation of a Highlight (#312, the #406 image-claim pattern): a conditional UPDATE that stamps sound_enrich_claimed_at iff a sound is still requested-but-unlanded AND no claim newer than ttl is held. It reports whether THIS caller won (RowsAffected == 1). The single-clock lease (#421) applies: stamp and cutoff are both DB now(), the app contributes only the TTL magnitude. Tenant-free.
func (*Store) UnarchiveCampaign ¶
UnarchiveCampaign clears a campaign's archived_at, returning it to the active set, and returns the updated row (#269). A missing id yields ErrNotFound. Un-archiving does not restore any operator's cleared durable selection (that pointer was nulled on archive) — the campaign simply becomes selectable again.
It is TENANT-SCOPED (#473): the UPDATE matches (id, tenant_id), so a foreign-tenant id is invisible and yields ErrNotFound.
func (*Store) UnpinnedNodes ¶ added in v0.5.0
UnpinnedNodes returns the Campaign's Nodes that are NOT pinned on a given Map, restricted to the types worth placing (#538): Locations, NPCs and Items are things that occupy space; Factions, Plot threads and Notes are not.
It backs the Maps tab's "unpinned entries" tray, which is what makes placing the world a drag rather than a form.
func (*Store) UpdateAgent ¶
UpdateAgent updates an Agent's editor fields and returns the updated row. It never changes agent_role, and it force-keeps a Butler's address_only true (the Butler always waits to be named, ADR-0024) — so editing the Butler can neither demote it nor turn off Address-Only. A missing id yields ErrNotFound.
func (*Store) UpdateBoard ¶ added in v0.5.0
func (s *Store) UpdateBoard(ctx context.Context, campaignID, id uuid.UUID, name string, nodeIDs []uuid.UUID) error
UpdateBoard renames a board AND replaces its entries in ONE transaction.
The two were separate calls, so a failed entry write (a stale node id, an entry deleted by a concurrent edit) left the board renamed with its old contents and returned an opaque error — a half-applied save the GM has no way to reason about. A board edit is one edit.
func (*Store) UpdateCampaign ¶
UpdateCampaign writes a campaign's name/system/language (and tape_armed when set) and bumps updated_at, returning the updated row. It is TENANT-SCOPED (#473): the UPDATE matches (id, tenant_id), so a foreign-tenant id is invisible and yields ErrNotFound (the RPC layer maps it to Connect CodeNotFound) — a cross-tenant write can never land.
func (*Store) UpdateCharacter ¶
UpdateCharacter saves a Character's editor fields (name/aliases/discord_user_id) and returns the updated row, stamping updated_at = now(). The write is scoped to (id, campaign_id) (#342), so a Character in another Campaign matches no row and yields ErrNotFound — a cross-campaign mutation is refused server-side without a separate ownership SELECT. Rebinding discord_user_id is a normal field write; a collision with another Character's (campaign, discord_user_id) yields ErrConflict. A missing id yields ErrNotFound.
func (*Store) UpdateEdgeDetails ¶ added in v0.5.0
func (s *Store) UpdateEdgeDetails(ctx context.Context, campaignID, id uuid.UUID, note string, disposition int) (KGEdge, error)
UpdateEdgeDetails saves an Edge's note and disposition, scoped to its Campaign (#342, #546). The relation TYPE is not touched: retyping an edge is deleting one and creating another, since the type carries validity rules.
func (*Store) UpdateMap ¶ added in v0.5.0
func (s *Store) UpdateMap(ctx context.Context, u CampaignMapUpdate) (CampaignMap, error)
UpdateMap saves a Map's editor fields, scoped to (id, campaign_id) (#342).
A Map may not become its own parent; deeper cycles are refused by Store.MapAncestors at read time rather than by a recursive CHECK, since the hierarchy is shallow and a GM re-parenting into a loop is a mistake to report, not a constraint violation to crash on.
func (*Store) UpdateNode ¶
UpdateNode saves a Knowledge Graph Node's editor fields (name/body/gm_private) and returns the updated row, stamping updated_at = now(). node_type is never touched (immutable, ADR-0008). The write is scoped to (id, campaign_id) (#342), so a Node in another Campaign matches no row and yields ErrNotFound — a cross-campaign mutation is refused server-side. A missing id yields ErrNotFound.
func (*Store) UpdateNodeWithAspects ¶ added in v0.5.0
func (s *Store) UpdateNodeWithAspects(ctx context.Context, u KGNodeUpdate, w KGNodeAspectWrite) (KGNode, error)
UpdateNodeWithAspects saves a Node's editor fields and its Aspects in ONE transaction (#542), so a save is never half-applied — the editor's name change and its aspect edits are one act to the GM and must be one act to the database. A missing Node yields ErrNotFound with nothing written.
func (*Store) UpdatePin ¶ added in v0.5.0
UpdatePin saves a Pin's position and presentation, scoped to its Campaign (#342). Out-of-range coordinates are refused by the DB CHECK.
func (*Store) UpsertProviderConfigs ¶
func (s *Store) UpsertProviderConfigs(ctx context.Context, configs []NewProviderConfig) ([]ProviderConfig, error)
UpsertProviderConfigs inserts-or-replaces a batch of Provider Configs in one transaction, keyed by (tenant_id, component, provider): a matching row has its model + sealed credential + last4 refreshed (the operator replacing a key, or the first real key overwriting the seed's "env" placeholder), otherwise a new row is inserted. The batch is atomic so a multi-Component provider (ElevenLabs → stt + tts share one key, ADR-0004) lands all-or-nothing. Returns the resulting rows in input order. Requires the UNIQUE (tenant_id, component, provider) key from migration 00005.
func (*Store) UpsertTapeConsent ¶
func (s *Store) UpsertTapeConsent(ctx context.Context, campaignID uuid.UUID, discordUserID string) error
UpsertTapeConsent records that a Speaker has consented to the rollover tape for a Campaign (#306, ADR-0051). It is idempotent: consenting twice keeps the original created_at. The (campaign_id, discord_user_id) primary key makes the row's presence the single source of truth for "this Speaker consented".
func (*Store) UpsertToolGrant ¶
func (s *Store) UpsertToolGrant(ctx context.Context, g NewToolGrant) error
UpsertToolGrant grants a Tool to an Agent, or edits an existing grant's scope Config in place — the #117 mutation path. It INSERTs the (agent_id, tool_name) row when absent and UPDATEs its Config when present, keyed off the UNIQUE(agent_id, tool_name) index, so "grant on" and "edit scope" are one call and a repeated grant never trips the unique constraint. An empty Config stores SQL NULL (no narrowing — dice's shape), so re-upserting nil clears a prior scope. The Agent's next Voice Session hydrates the resulting row (#113).
func (*Store) UpsertTranscriptLine ¶
func (s *Store) UpsertTranscriptLine(ctx context.Context, l TranscriptLine) error
UpsertTranscriptLine writes (or updates in place) one transcript Line. An Agent reply coalesces across its sentences under one line_id, so a re-write of the same (voice_session_id, line_id) updates the text/ts rather than inserting a new row — keeping COUNT(*) == distinct lines. seq is deliberately NOT updated on conflict (#149): it is the replay ordering key, fixed at insert time, so ListTranscriptLines (ORDER BY seq) matches the live-view order even when an interleaved line landed between a reply's sentences.
func (*Store) UpsertUser ¶
UpsertUser inserts a user keyed by discord_user_id, or refreshes the display name/avatar of the existing row (Discord is the source of truth for those on every login). It returns the resulting user. The role is preserved on conflict so an operator promotion is never clobbered by a login.
func (*Store) VoiceSessionInTenant ¶ added in v0.2.1
func (s *Store) VoiceSessionInTenant(ctx context.Context, tenantID, sessionID uuid.UUID) (bool, error)
VoiceSessionInTenant reports whether the Voice Session belongs to the Tenant (session → campaign → tenant). It backs the transcript snapshot/SSE mounts' tenant-scoped 404 posture (#439, via transcript.TenantScope): false covers both a foreign-tenant session and one that does not exist at all, so the caller's 404 never reveals which.
type Subscription ¶ added in v0.2.1
type Subscription struct {
ID uuid.UUID
TenantID uuid.UUID
PlanID uuid.UUID
PlanSlug string
MonthlyPriceUSD float64
StartedAt time.Time
EndedAt *time.Time
}
Subscription is a Tenant's binding to a Plan. PlanSlug and MonthlyPriceUSD are snapshots taken at subscribe time (revenue history survives catalog edits); EndedAt is nil while the subscription is active.
type TaggedNode ¶ added in v0.5.0
TaggedNode is one (node, tag) pair — the campaign-wide read the list and graph filters index client-side, so tagging a hundred entries is still one round trip.
type Tenant ¶
type Tenant struct {
ID uuid.UUID
Name string
// SpendCapSoftUSD / SpendCapHardUSD are the two independently opt-in per-Tenant
// spend caps (#130, ADR-0046): nil = that cap is off. They gate a Voice Session's
// estimated spend (an approximate figure, never a billed amount).
SpendCapSoftUSD *float64
SpendCapHardUSD *float64
CreatedAt time.Time
UpdatedAt time.Time
}
Tenant is the top-level isolation boundary.
type TenantBillingLine ¶ added in v0.2.1
type TenantBillingLine struct {
TenantID uuid.UUID
TenantName string
PlanSlug string // empty: no subscription overlapped the window
MonthlyPriceUSD float64 // 0 when PlanSlug is empty
EstimatedUSD float64
LLMInputTokens int64
LLMOutputTokens int64
TTSCharacters int64
STTAudioSeconds float64
}
TenantBillingLine is one tenant's row in the billing report window: the active or overlapping subscription snapshot(s) plus the ledger's summed estimated cost. A tenant that switched plans mid-window appears once per subscription (revenue is per subscription row); usage is attached to the FIRST line only so summing the report never double-counts cost.
type TenantOperatorBinding ¶ added in v0.4.0
TenantOperatorBinding pairs a Tenant with its operator's Discord snowflake — one row of the per-Tenant GM-identity source (#490). It lets GMIdentity scope GM standing to the OWNING Tenant (IsGMInTenant) instead of the deployment-wide union ListTenantOperatorDiscordIDs feeds, closing ADR-0055's deployment-scope caveat: a Tenant A operator is GM in Tenant A only, never in Tenant B's Guild.
type TenantPlanRow ¶ added in v0.2.1
type TenantPlanRow struct {
ID uuid.UUID
Name string
CreatedAt time.Time
PlanSlug string // empty: no active subscription
}
TenantPlanRow is one line of ListTenantsWithPlan.
type ToolGrant ¶
type ToolGrant struct {
ID uuid.UUID
AgentID uuid.UUID
ToolName string
// Surface is which Agent surface the grant arms (#592, ADR-0062): chat
// Tool Grants are separate rows beside the voice grants, so granting the
// Butler a chat tool never widens what it may call in voice (ADR-0029).
Surface GrantSurface
Config json.RawMessage
CreatedAt time.Time
UpdatedAt time.Time
}
ToolGrant is an Agent's persisted permission to invoke one named Tool (ADR-0029) — the DB shape of the in-memory Grant the live loop hydrates into a GrantSet (#113). Config is the optional per-grant scope/config (jsonb): nil when the grant carries no narrowing (dice), a scope blob for a Tool granted differently per Agent. It reaches the Tool handler at execution time and is enforced there, never by the LLM.
type TranscriptChunk ¶
type TranscriptChunk struct {
ID uuid.UUID
CampaignID uuid.UUID
VoiceSessionID uuid.UUID // column nullable; the writer always sets it
Content string // the chunk's utterances joined "\n"
SpeakerDiscordUserIDs []string // distinct Speaker Lane snowflakes in the chunk (populated since #278/ADR-0050); empty only when all utterances were unattributed
ParticipatedAgentIDs []uuid.UUID // Agents that spoke in the chunk (NPC-knowledge filter)
EmbeddingModel string // ” until the backfill worker embeds the row (#116)
StartedAt time.Time
CreatedAt time.Time
}
TranscriptChunk is one persisted Transcript Chunk. embedding is always NULL at insert (the async embedding pipeline, ADR-0011); EmbeddingModel is empty until the backfill worker (#116) embeds the row and stamps which model produced the vector — the provenance a model-switch re-embed pass keys off (ADR-0011).
type TranscriptLine ¶
type TranscriptLine struct {
VoiceSessionID uuid.UUID
CampaignID uuid.UUID
LineID string
Seq int64
Who string
Tag string
Kind string
TS time.Time
Text string
// SpeakerDiscordUserID is the Discord snowflake of the human who spoke this Line
// (#278, ADR-0050), or "" for an unattributed utterance / an Agent reply. It
// round-trips "" ↔ NULL: NULLIF on write, COALESCE on scan.
SpeakerDiscordUserID string
}
TranscriptLine is one persisted transcript Line. LineID is the relay's stable Line.ID; Seq is the relay's monotonic per-session ordering key (Frame.Seq).
type UpsertUserParams ¶
UpsertUserParams is the input to UpsertUser: the Discord identity to insert or refresh. Role is not an input — a new user defaults to 'operator' (the DB default) and an existing user's role is left untouched on refresh.
type UsageRow ¶ added in v0.2.1
type UsageRow struct {
TenantID uuid.UUID
Day time.Time
Component Component
Provider string
Model string
LLMInputTokens int64
LLMOutputTokens int64
TTSCharacters int64
STTAudioSeconds float64
EstimatedUSD float64
}
UsageRow is one daily-bucketed usage accumulation the ledger sink flushes (ADR-0054). Day is a calendar date (UTC); only its date part is stored. EstimatedUSD is priced from the static map at capture time — an ESTIMATE, never billing truth (ADR-0046 posture).
type User ¶
type User struct {
ID uuid.UUID
DiscordUserID string
Name string
// Avatar is an absolute image URL (or empty).
Avatar string
Role string
CreatedAt time.Time
UpdatedAt time.Time
// SuspendedAt, when non-nil, marks the user locked out under the
// open-Admission-Mode revocation mechanism (ADR-0055).
SuspendedAt *time.Time
// AUPAcceptedAt records when the user last acknowledged the
// Nutzungsbedingungen + Datenschutzerklärung at an open-mode OAuth start
// (#518). nil for allowlist-mode logins (not gated) and pre-column
// accounts. Appended LAST — userColumns/scanUser are column-order-coupled.
AUPAcceptedAt *time.Time
}
User is a human operator authenticated via Discord OAuth (ADR-0016). The Discord snowflake is the stable identity key; Name/Avatar are display-only and refreshed from Discord on each login.
type VoiceSession ¶
type VoiceSession struct {
ID uuid.UUID
CampaignID uuid.UUID
StartedAt time.Time
EndedAt *time.Time
Status VoiceSessionStatus
LineCount int
EndReason *string
}
VoiceSession is one run of the live voice loop — the Bot's presence in one Discord voice channel, bound to a Campaign (CONTEXT.md "Voice Session", #72). EndedAt is nil while running; LineCount records transcript lines produced (0 for this stage — the live feed is #73). EndReason is nil for a clean end, and set when the boot reconciliation closed an orphaned row (#143) or a fatal gateway rejection ended the session as 'failed' (#123, the readable cause).
type VoiceSessionControl ¶ added in v0.4.0
type VoiceSessionControl struct {
ID uuid.UUID
IntentID uuid.UUID
TenantID uuid.UUID
Kind VoiceSessionControlKind
AgentID string
SayText string
Muted bool
// DirectTurns is the 'direct' verb's committed-turn bound (ADR-0059): how
// many of the Agent's committed turns the directive rides; 0 = sticky.
DirectTurns int
Status VoiceSessionControlStatus
ResultIDs []string
LastError string
CreatedAt time.Time
StartedAt *time.Time
EndedAt *time.Time
}
VoiceSessionControl is one row of the requested-control queue (#503).
type VoiceSessionControlKind ¶ added in v0.4.0
type VoiceSessionControlKind string
VoiceSessionControlKind names one control verb.
const ( // VoiceControlMuteAgent mutes/unmutes one voiced Agent (Manager.SetAgentMute). VoiceControlMuteAgent VoiceSessionControlKind = "mute_agent" // VoiceControlMuteAll mutes/unmutes every voiced Agent (Manager.SetAllMute). VoiceControlMuteAll VoiceSessionControlKind = "mute_all" // VoiceControlSay makes one voiced Agent speak SayText (Manager.SayAs). VoiceControlSay VoiceSessionControlKind = "say" // VoiceControlButlerSay makes the Butler speak SayText (Manager.SpeakAsButler) // — the voiced-recap relay. VoiceControlButlerSay VoiceSessionControlKind = "butler_say" // VoiceControlDirect sets/clears one Agent's GM directive (Manager.DirectAs, // ADR-0059): SayText carries the directive text (” clears), DirectTurns the // committed-turn bound (0 = sticky until cleared/replaced/session end). VoiceControlDirect VoiceSessionControlKind = "direct" )
type VoiceSessionControlStatus ¶ added in v0.4.0
type VoiceSessionControlStatus string
VoiceSessionControlStatus is a control row's lifecycle state.
const ( // VoiceControlPending: written by the requester, not yet claimed. VoiceControlPending VoiceSessionControlStatus = "pending" // VoiceControlExecuting: the hosting worker fenced a pending→executing claim // and is running the verb. A transient finish-write failure leaves the row // here (never back to pending), so it can never re-dispatch (#503 FIX1 — say // is not idempotent); the requester treats it as non-terminal and keeps // polling, and the sweep retires a row stranded here. VoiceControlExecuting VoiceSessionControlStatus = "executing" // VoiceControlDone: the hosting worker executed it successfully. VoiceControlDone VoiceSessionControlStatus = "done" // VoiceControlFailed: execution failed (LastError carries the encoded cause), // the requester timed out, or the session ended with the row unfinished. VoiceControlFailed VoiceSessionControlStatus = "failed" )
type VoiceSessionIntent ¶ added in v0.4.0
type VoiceSessionIntent struct {
ID uuid.UUID
TenantID uuid.UUID
CampaignID uuid.UUID
// VoiceChannelID is the voice channel the start explicitly picked (” = use
// the guild's Default Voice Channel); the claiming worker passes it through
// to Manager.Start.
VoiceChannelID string
Status VoiceSessionIntentStatus
InstanceID string
VoiceSessionID uuid.NullUUID
StopRequested bool
LastError string
CreatedAt time.Time
ClaimedAt *time.Time
HeartbeatAt *time.Time
EndedAt *time.Time
}
VoiceSessionIntent is one row of the voice-session claim plane (#491): a tenant-keyed intent a web Start writes and a -mode voice worker claims, runs, and heartbeats. VoiceSessionID is set once the worker goes live; ClaimedAt / HeartbeatAt / EndedAt track the claim lifecycle; LastError carries a fault's readable cause. Mirrors the storage.Job shape (ADR-0049).
type VoiceSessionIntentStatus ¶ added in v0.4.0
type VoiceSessionIntentStatus string
VoiceSessionIntentStatus is a Voice Session Intent's lifecycle state (#491, ADR-0057 (b)): the claim-plane row a web-tier Start writes and a -mode voice worker claims. 'pending' → 'claimed' (a worker took it) → 'live' (its loop is up) → 'done' (clean end) / 'failed' (loop fault) / 'dead' (the worker's heartbeat went stale — no takeover, ADR-0006). The three non-terminal states share the one-live-per-tenant partial UNIQUE index.
const ( // VoiceIntentPending: written by Start, not yet claimed by any worker. VoiceIntentPending VoiceSessionIntentStatus = "pending" // VoiceIntentClaimed: a worker won the FOR UPDATE SKIP LOCKED claim and stamped // its instance_id; its loop is starting but not yet live. VoiceIntentClaimed VoiceSessionIntentStatus = "claimed" // VoiceIntentLive: the worker's voice loop is up and the voice_sessions row is // bound; the worker heartbeats while in this state. VoiceIntentLive VoiceSessionIntentStatus = "live" // VoiceIntentDone: terminal clean end (Stop honored, or the loop self-exited). VoiceIntentDone VoiceSessionIntentStatus = "done" // VoiceIntentDead: terminal — the owning worker's heartbeat went stale, so the // reaper marked it dead. No mid-session takeover (ADR-0006/0057 (e)): the Tenant // restarts; the session is NEVER handed to another Voice Instance. VoiceIntentDead VoiceSessionIntentStatus = "dead" // VoiceIntentFailed: terminal — the claiming worker could not run the session // (e.g. the Manager refused it), last_error carries the readable cause. VoiceIntentFailed VoiceSessionIntentStatus = "failed" )
type VoiceSessionStatus ¶
type VoiceSessionStatus string
VoiceSessionStatus is a Voice Session's lifecycle state (#72). A session is 'running' from Start until Stop (or loop exit), then 'ended' — or 'failed' when a fatal, non-retryable gateway rejection ended it (#123).
const ( VoiceSessionRunning VoiceSessionStatus = "running" VoiceSessionEnded VoiceSessionStatus = "ended" // VoiceSessionFailed is the terminal state of a session whose Discord gateway // connection failed FATALLY — a non-retryable rejection (invalid Bot token, // disallowed intents, gateway reject) the reconnect loop stopped on rather than // backing off forever (#123). The row's end_reason carries the readable cause. // Like 'ended' it is terminal (never revived), but records that the session // never served — distinct from a clean stop. VoiceSessionFailed VoiceSessionStatus = "failed" )
Source Files
¶
- auth.go
- billing.go
- campaign_archive.go
- campaign_map.go
- campaign_share.go
- character.go
- deployment.go
- highlight.go
- highlight_search.go
- job.go
- kg_draft.go
- kg_edge.go
- kg_graph.go
- kg_node.go
- kg_node_aspect.go
- kg_node_portrait.go
- kg_node_search.go
- kg_prompt.go
- kg_tag_board.go
- knowledge_proposal.go
- map_pin.go
- migrate.go
- models.go
- node_appearance.go
- party_marker.go
- planning_chat.go
- presenceowner.go
- settings.go
- signup.go
- spend.go
- store.go
- tape_consent.go
- tool_grant.go
- tool_grant_mapping.go
- tracer.go
- transcript_chunk.go
- transcript_line.go
- voice_mapping.go
- voice_session.go
- voicecontrol.go
- voiceintent.go
- write.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package crypto encrypts BYOK Provider Config credentials at rest (ADR-0004): AES-256-GCM with a single app-level secret supplied via env var.
|
Package crypto encrypts BYOK Provider Config credentials at rest (ADR-0004): AES-256-GCM with a single app-level secret supplied via env var. |
|
Package migrations holds the embedded SQL migration files for the Glyphoxa schema.
|
Package migrations holds the embedded SQL migration files for the Glyphoxa schema. |