Documentation
¶
Index ¶
- Constants
- Variables
- func AgentDir(id string) string
- func BuildMCPServers(agentID, apiBase string, hasSlackBot bool) map[string]mcpServerEntry
- func DefaultAgentWorkDir(agentID string) (string, error)
- func DeleteAvatar(bs *blob.Store, agentID string) error
- func EnsureAgentWorkspaceDirIfDefault(workDir, agentID string) error
- func GenerateAvatarWithAI(ctx context.Context, agentID string, persona string, name string, ...) (string, error)
- func GenerateName(persona string, userPrompt string) (string, error)
- func GeneratePersona(currentPersona string, userPrompt string) (string, error)
- func GeneratePublicProfile(persona string) (string, error)
- func GenerateSVGAvatarFile(name string) (string, error)
- func GenerateTOTPCode(secret, algorithm string, digits, period int) (string, int64, error)
- func GuideDir() string
- func IsAllowedImageExt(ext string) bool
- func IsHubLocalSafePatchKey(k string) bool
- func IsInSilentHours(start, end string) bool
- func IsPathSafeAgentID(id string) bool
- func LoadGeminiAPIKey(creds *CredentialStore) (string, error)
- func LoadXAIAPIKey(creds *CredentialStore) (string, error)
- func LockAgentMemorySync(agentID string) func()
- func LookupAgentToken(agentID string) (string, bool)
- func LookupPeerCount() int
- func NormalizeTOTPSecret(secret string) (string, error)
- func NormalizeThinkingMode(mode string) string
- func ParseCronSchedule(expr string) (cron.Schedule, error)
- func PreCompactSummarize(agentID string, tool string, transcriptPath string, logger *slog.Logger) error
- func PrepareClaudeSettings(agentID, apiBase string, allowProtectedPaths []string, logger *slog.Logger)
- func ReadCheckinFileOrDefault(agentID string) (string, bool, error)
- func ReadClaudeSessionFiles(agentID string) ([]ClaudeSessionFile, []SkippedSessionFile, error)
- func ReadCodexSessionFiles(agentID string) (*CodexSessionTransfer, []SkippedSessionFile, error)
- func ReadGrokSessionFiles(agentID string) (*GrokSessionTransfer, []SkippedSessionFile, error)
- func ReadUserFileOrDefault(agentID string) (string, bool, error)
- func ReadWorkspaceFile(ctx context.Context, st *store.Store, agentID string, ...) (body string, isDefault bool, etag string, err error)
- func RecentDiarySummary(agentID string) string
- func ReconcileAgentDiskFromDB(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
- func ReconcileAgentDiskFromDBHeld(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
- func ReconcileWorkspaceFilesDiskFromDBHeld(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
- func ResolveCronPreset(expr, agentID string) string
- func SaveAvatar(bs *blob.Store, agentID string, src io.Reader, ext string) error
- func ServeAvatar(bs *blob.Store, w http.ResponseWriter, r *http.Request, a *Agent)
- func SetAgentTokenLookup(fn func(string) (string, bool))
- func SetKojoAPIBase(base string)
- func SetPeerCountLookup(fn func() int)
- func StageClaudeSessionFiles(agentID string, files []ClaudeSessionFile) (commit func(), rollback func(), err error)
- func StageCodexSession(agentID string, transfer *CodexSessionTransfer) (commit func(), rollback func(), err error)
- func StageCodexSessionCleanup(agentID string) (commit func(), rollback func(), err error)
- func StageGrokSession(agentID string, transfer *GrokSessionTransfer) (commit func(), rollback func(), err error)
- func StageGrokSessionCleanup(agentID string) (commit func(), rollback func(), err error)
- func SyncAgentMemoryFromDisk(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
- func SyncAgentMemoryFromDiskBestEffort(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) (ran bool, err error)
- func SyncAgentPersonaFromDisk(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
- func SyncAttachSkill(agentID string, enabled bool, logger *slog.Logger)
- func SyncAttachSkillForTool(agentID, tool string, enabled bool, logger *slog.Logger)
- func SyncCodexAttachSkill(agentID string, enabled bool, logger *slog.Logger)
- func SyncCodexDeviceSwitchSkill(agentID string, enabled bool, logger *slog.Logger)
- func SyncDeviceSwitchSkill(agentID string, enabled bool, logger *slog.Logger)
- func SyncDeviceSwitchSkillForTool(agentID, tool string, enabled bool, logger *slog.Logger)
- func SyncGrokDeviceSwitchSkill(agentID string, enabled bool, logger *slog.Logger)
- func SyncGuides(logger *slog.Logger) bool
- func SyncWorkspaceFilesFromDisk(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
- func TurnSummarize(agentID string, tool string, logger *slog.Logger) error
- func ValidEffort(effort string) bool
- func ValidInjectionKey(key string) bool
- func ValidModelEffort(model, effort string) bool
- func ValidResumeIdle(minutes int) bool
- func ValidSilentHours(start, end string) error
- func ValidThinkingMode(mode string) bool
- func ValidTimeout(minutes int) bool
- func ValidateCronExpr(expr string) error
- func ValidateDisabledInjections(keys []string) error
- func ValidateTOTPParams(secret, algorithm string, digits, period int) (string, error)
- func ValidateTTS(c *TTSConfig) error
- func ValidateTempAvatarPath(avatarPath string) (string, error)
- func WriteCheckinFile(ctx context.Context, st *store.Store, agentID, content string) error
- func WriteUserFile(ctx context.Context, st *store.Store, agentID, content string) error
- func WriteWorkspaceFile(ctx context.Context, st *store.Store, agentID string, ...) (*store.AgentWorkspaceFileRecord, error)
- type Agent
- type AgentConfig
- type AgentTokenStore
- type AgentUpdateConfig
- type AnswerFunc
- type ArrivalNotes
- type AttachmentForwarder
- type BackgroundTurnFunc
- type BusySource
- type ChatBackend
- type ChatEvent
- type ChatOptions
- type ClaudeBackend
- func (b *ClaudeBackend) Available() bool
- func (b *ClaudeBackend) Chat(ctx context.Context, agent *Agent, userMessage string, systemPrompt string, ...) (<-chan ChatEvent, error)
- func (b *ClaudeBackend) CloseAllSessions()
- func (b *ClaudeBackend) CloseSession(agentID string)
- func (b *ClaudeBackend) CloseSessionSync(agentID string)
- func (b *ClaudeBackend) HasLiveSession(agentID string) bool
- func (b *ClaudeBackend) Name() string
- func (b *ClaudeBackend) SessionInTurn(agentID string) bool
- func (b *ClaudeBackend) SetBackgroundTurnHandler(fn BackgroundTurnFunc)
- func (b *ClaudeBackend) SetProxyURL(url string)
- func (b *ClaudeBackend) SetRateLimitHandler(fn func(agentID string, info RateLimitInfo))
- func (b *ClaudeBackend) SetSubagentActivityHandler(fn subagentActivityFunc)
- type ClaudeSessionFile
- type CodexBackend
- type CodexSQLiteRow
- type CodexSQLiteValue
- type CodexSessionTransfer
- type CodexThreadTransfer
- type Credential
- type CredentialStore
- func (s *CredentialStore) AddCredential(agentID, label, username, password string, totp *TOTPParams) (*Credential, error)
- func (s *CredentialStore) Close() error
- func (s *CredentialStore) DeleteAllForAgent(agentID string) error
- func (s *CredentialStore) DeleteCredential(agentID, credID string) error
- func (s *CredentialStore) DeleteToken(provider, agentID, sourceID, key string) error
- func (s *CredentialStore) DeleteTokensByAgent(agentID string) error
- func (s *CredentialStore) DeleteTokensBySource(provider, agentID, sourceID string) error
- func (s *CredentialStore) ExportCredentials(agentID string) ([]*Credential, error)
- func (s *CredentialStore) GetSetting(key string) string
- func (s *CredentialStore) GetTOTPCode(agentID, credID string) (string, int64, error)
- func (s *CredentialStore) GetToken(provider, agentID, sourceID, key string) (string, error)
- func (s *CredentialStore) ListCredentials(agentID string) ([]*Credential, error)
- func (s *CredentialStore) ReplaceCredentials(agentID string, creds []*Credential) error
- func (s *CredentialStore) RevealPassword(agentID, credID string) (string, error)
- func (s *CredentialStore) SetSetting(key, value string) error
- func (s *CredentialStore) SetToken(provider, agentID, sourceID, key, value string, expiresAt time.Time) error
- func (s *CredentialStore) UpdateCredential(agentID, credID string, label, username, password *string, totp *TOTPParams) (*Credential, error)
- type CustomBackend
- type DirectoryEntry
- type ForkOptions
- type GrokBackend
- type GrokSessionFile
- type GrokSessionTransfer
- type GroupDM
- type GroupDMKind
- type GroupDMManager
- func (m *GroupDMManager) APIBase() string
- func (m *GroupDMManager) AddMember(id, newAgentID, callerAgentID string) (*GroupDM, error)
- func (m *GroupDMManager) CheckMembership(groupID, agentID string) error
- func (m *GroupDMManager) ClearMessages(ctx context.Context, groupID string) (int64, error)
- func (m *GroupDMManager) Create(name string, memberIDs []string, cooldown int, style GroupDMStyle, ...) (*GroupDM, error)
- func (m *GroupDMManager) CreateThread(agentID string) (*GroupDM, error)
- func (m *GroupDMManager) CreateWithNotify(name string, memberIDs []string, cooldown int, style GroupDMStyle, ...) (*GroupDM, error)
- func (m *GroupDMManager) DeadLetters(ctx context.Context, groupID string, limit int) ([]*store.GroupDMDeadLetter, error)
- func (m *GroupDMManager) Delete(id string, notify bool) error
- func (m *GroupDMManager) FindOrCreateDM(memberIDs []string) (*GroupDM, bool, error)
- func (m *GroupDMManager) Get(id string) (*GroupDM, bool)
- func (m *GroupDMManager) GroupsForAgent(agentID string) []*GroupDM
- func (m *GroupDMManager) LatestMessageID(groupID string) string
- func (m *GroupDMManager) LeaveGroup(id, agentID string) error
- func (m *GroupDMManager) List() []*GroupDM
- func (m *GroupDMManager) LockPatch(groupID string) (release func())
- func (m *GroupDMManager) MarkGroupRead(groupID, messageID string) error
- func (m *GroupDMManager) Messages(groupID string, limit int, before string) ([]*GroupMessage, bool, string, error)
- func (m *GroupDMManager) PostMessage(ctx context.Context, groupID, agentID, content, expectedLatestID string, ...) (*GroupMessage, error)
- func (m *GroupDMManager) PostMessageStrict(ctx context.Context, groupID, agentID, content, expectedLatestID string, ...) (*GroupMessage, error)
- func (m *GroupDMManager) PostSteerMessage(ctx context.Context, groupID, content string, attachments []MessageAttachment) (*GroupMessage, error)
- func (m *GroupDMManager) PostUserMessage(ctx context.Context, groupID, content string, attachments []MessageAttachment, ...) (*GroupMessage, error)
- func (m *GroupDMManager) RemoveAgent(agentID string)
- func (m *GroupDMManager) Rename(id, name, callerAgentID string) (*GroupDM, error)
- func (m *GroupDMManager) SetAPIBase(base string)
- func (m *GroupDMManager) SetCooldown(id string, seconds int) (*GroupDM, error)
- func (m *GroupDMManager) SetMemberNotifyMode(groupID, agentID string, mode NotifyMode, digestWindow int, ...) (*GroupDM, error)
- func (m *GroupDMManager) SetOneShotForTesting(...)
- func (m *GroupDMManager) SetStyle(id string, style GroupDMStyle, callerAgentID string) (*GroupDM, error)
- func (m *GroupDMManager) SetVenue(id string, venue GroupDMVenue, callerAgentID string) (*GroupDM, error)
- func (m *GroupDMManager) Steer(ctx context.Context, groupID, content string) (*GroupMessage, error)
- func (m *GroupDMManager) StopThreadTurn(groupID string) bool
- func (m *GroupDMManager) ThreadLive(groupID string) (active bool, snapshot ThreadLiveSnapshot)
- func (m *GroupDMManager) UnreadInfo(groupID, afterID string, limit int, useOperatorCursor bool) (count int, mentionsUser, hasMore bool, err error)
- func (m *GroupDMManager) UpdateSettings(ctx context.Context, id string, patch GroupDMSettingsPatch) (*GroupDM, string, error)
- type GroupDMSettingsPatch
- type GroupDMStyle
- type GroupDMVenue
- type GroupMember
- type GroupMessage
- type LlamaCppBackend
- type Manager
- func (m *Manager) Abort(agentID string)
- func (m *Manager) AcquireMutation(agentID string) (func(), error)
- func (m *Manager) ActivateAgentRuntime(agentID string)
- func (m *Manager) ActiveTasksSummary(ctx context.Context, agentID string) string
- func (m *Manager) AgentTokenStore() AgentTokenStore
- func (m *Manager) AnswerQuestion(ctx context.Context, agentID, requestID string, answers map[string]any, ...) error
- func (m *Manager) Archive(id string) error
- func (m *Manager) ArmRestartWake(agentID, sessionKey string) error
- func (m *Manager) BackendAvailability() map[string]bool
- func (m *Manager) BlobStore() *blob.Store
- func (m *Manager) BuildRecentMessagesContext(parent context.Context, agentID string) string
- func (m *Manager) BuildVolatileContext(ctx context.Context, agentID string, queryContext string) string
- func (m *Manager) BusySince(agentID string) (time.Time, bool)
- func (m *Manager) CanResumeSession(agentID, sessionKey string) bool
- func (m *Manager) CancelOneShotsForAgent(agentID string)
- func (m *Manager) Chat(ctx context.Context, agentID string, userMessage string, role string, ...) (<-chan ChatEvent, error)
- func (m *Manager) ChatOneShot(ctx context.Context, agentID string, userMessage string, opts OneShotOpts) (<-chan ChatEvent, error)
- func (m *Manager) Checkin(agentID string) error
- func (m *Manager) ClearAgentArrivedHere(ctx context.Context, agentID string) error
- func (m *Manager) ClearAgentReleasedHere(ctx context.Context, agentID string) error
- func (m *Manager) ClearAllEmbeddings()
- func (m *Manager) Close() error
- func (m *Manager) CloseAllClaudeSessions()
- func (m *Manager) CloseAllIndexes()
- func (m *Manager) CloseClaudeSession(agentID string)
- func (m *Manager) CloseClaudeSessionSync(agentID string)
- func (m *Manager) ConsumeRestartWake(version string, bootTime time.Time)
- func (m *Manager) Create(cfg AgentConfig) (*Agent, error)
- func (m *Manager) CreateAgentMemoryEntry(ctx context.Context, agentID, kind, name, body string) (*MemoryEntryRecord, error)
- func (m *Manager) CreateTask(ctx context.Context, agentID string, params TaskCreateParams) (*Task, error)
- func (m *Manager) Credentials() *CredentialStore
- func (m *Manager) CronPaused() bool
- func (m *Manager) Delete(id string) error
- func (m *Manager) DeleteAgentMemory(ctx context.Context, agentID, ifMatchETag string) error
- func (m *Manager) DeleteAgentMemoryEntry(ctx context.Context, agentID, entryID, ifMatchETag string) error
- func (m *Manager) DeleteAllTasks(ctx context.Context, agentID string)
- func (m *Manager) DeleteMessage(agentID, msgID, ifMatchETag string) error
- func (m *Manager) DeleteMirrorForAgent(agentID string) error
- func (m *Manager) DeleteTask(ctx context.Context, agentID, taskID, ifMatchETag string) error
- func (m *Manager) Directory() []DirectoryEntry
- func (m *Manager) DisarmRestartWake()
- func (m *Manager) DrainBlockers() []string
- func (m *Manager) EvictNonLocalAgentsAtStartup(ctx context.Context, selfPeerID string)
- func (m *Manager) Fork(srcID string, opts ForkOptions) (*Agent, error)
- func (m *Manager) Get(id string) (*Agent, bool)
- func (m *Manager) GetAgentArrivedProxy(ctx context.Context, agentID string) (string, error)
- func (m *Manager) GetAgentMemory(ctx context.Context, agentID string) (*MemoryRecord, error)
- func (m *Manager) GetAgentMemoryEntry(ctx context.Context, agentID, entryID string) (*MemoryEntryRecord, error)
- func (m *Manager) GetAgentPersona(ctx context.Context, agentID string) (*PersonaRecord, error)
- func (m *Manager) GetRemote(id string) *Agent
- func (m *Manager) GetTask(ctx context.Context, agentID, taskID string) (*Task, error)
- func (m *Manager) HasCredentials() bool
- func (m *Manager) HasPendingQuestion(agentID string) bool
- func (m *Manager) HydrateAgentBlobsAtLoad()
- func (m *Manager) InFlightOneShotSessionKey(agentID string) string
- func (m *Manager) IsAgentActive(agentID string) (bool, bool)
- func (m *Manager) IsAgentDMAvailable(agentID string) (bool, bool)
- func (m *Manager) IsBusy(agentID string) bool
- func (m *Manager) IsBusyForStatus(agentID string) bool
- func (m *Manager) IsPrivileged(id string) bool
- func (m *Manager) IsSwitching(agentID string) bool
- func (m *Manager) List() []*Agent
- func (m *Manager) ListAgentMemoryEntries(ctx context.Context, agentID string, opts MemoryEntryListOptions) ([]*MemoryEntryRecord, error)
- func (m *Manager) ListArrivedAgents(ctx context.Context) ([]string, error)
- func (m *Manager) ListReleasedAgents(ctx context.Context) (map[string]struct{}, error)
- func (m *Manager) ListRemote() []*Agent
- func (m *Manager) ListTasks(ctx context.Context, agentID string) ([]*Task, error)
- func (m *Manager) LockPatch(id string) (release func())
- func (m *Manager) MarkAgentArrivedHere(ctx context.Context, agentID, allowedProxyPeer string) error
- func (m *Manager) MarkAgentReleasedHere(ctx context.Context, agentID string) error
- func (m *Manager) Messages(agentID string, limit int) ([]*Message, error)
- func (m *Manager) MessagesPaginated(agentID string, limit int, before string) ([]*Message, bool, error)
- func (m *Manager) MirrorMessageExists(agentID, messageID string) (bool, error)
- func (m *Manager) MirrorMessagesPaginated(agentID string, limit int, before string) ([]*Message, bool, error)
- func (m *Manager) NextCronRun(agentID string) time.Time
- func (m *Manager) NotifyDeviceSwitchArrival(agentID, sourcePeerName, opID string, notes ArrivalNotes)
- func (m *Manager) PruneToOwnedAgentsForPeer(ctx context.Context, selfPeerID string) (ok bool)
- func (m *Manager) PutAgentMemory(ctx context.Context, agentID, body, ifMatchETag string) (*MemoryRecord, error)
- func (m *Manager) PutAgentPersona(ctx context.Context, agentID, body, ifMatchETag string) (*PersonaRecord, error)
- func (m *Manager) RateLimit(agentID string) (RateLimitSnapshot, bool)
- func (m *Manager) ReapplyHubLocalOverrides(ctx context.Context, agentID string) ([]string, []string, error)
- func (m *Manager) Regenerate(ctx context.Context, agentID, msgID, ifMatchETag string) error
- func (m *Manager) ReleaseAgentLocally(agentID string)
- func (m *Manager) ReloadAgentFromStore(agentID string) error
- func (m *Manager) ReplaceMirrorWindowFromMessages(agentID, holderPeer string, msgs []*Message) error
- func (m *Manager) ResetData(id string) error
- func (m *Manager) ResetSession(agentID string) error
- func (m *Manager) SetAttachmentForwarder(f AttachmentForwarder)
- func (m *Manager) SetBlobStore(bs *blob.Store)
- func (m *Manager) SetCronPaused(paused bool) error
- func (m *Manager) SetGroupDMManager(gdm *GroupDMManager)
- func (m *Manager) SetPrivileged(id string, privileged bool) error
- func (m *Manager) SetQuiescing(on bool)
- func (m *Manager) SetSwitching(agentID string, on bool) error
- func (m *Manager) SetTokenStore(ts AgentTokenStore)
- func (m *Manager) Shutdown()
- func (m *Manager) SnapshotAccumulatedMessageRecord(agentID string) *store.MessageRecord
- func (m *Manager) StartFileWatcher()
- func (m *Manager) StartSchedulers()
- func (m *Manager) Steer(ctx context.Context, agentID, text string) (string, error)
- func (m *Manager) SteerOneShot(sessionKey, text string) error
- func (m *Manager) Store() *store.Store
- func (m *Manager) Subscribe(agentID string) (startedAt time.Time, past []ChatEvent, live <-chan ChatEvent, unsub func(), ...)
- func (m *Manager) TeardownAgentRuntime(agentID string)
- func (m *Manager) TruncateMemoryAt(agentID string, since time.Time) (*TruncateMemoryResult, error)
- func (m *Manager) TruncateMemoryFromMessage(agentID, msgID string) (*TruncateMemoryResult, error)
- func (m *Manager) Unarchive(id string) error
- func (m *Manager) Update(id string, cfg AgentUpdateConfig) (*Agent, error)
- func (m *Manager) UpdateAgentMemoryEntry(ctx context.Context, agentID, entryID, ifMatchETag string, ...) (*MemoryEntryRecord, error)
- func (m *Manager) UpdateMessageContent(agentID, msgID, content, ifMatchETag string) (*Message, string, error)
- func (m *Manager) UpdateRemoteHubRow(id string, cfg AgentUpdateConfig, casIfMatch string) (*Agent, error)
- func (m *Manager) UpdateSlackBot(id string, cfg *SlackBotConfig) error
- func (m *Manager) UpdateSlackBotAlreadyGuarded(id string, cfg *SlackBotConfig) error
- func (m *Manager) UpdateTask(ctx context.Context, agentID, taskID, ifMatchETag string, ...) (*Task, error)
- func (m *Manager) UpsertMirrorFromMessages(agentID, holderPeer string, msgs []*Message) error
- func (m *Manager) WaitAllChatsIdle(ctx context.Context) error
- func (m *Manager) WaitChatDone(ctx context.Context, agentID string) *store.MessageRecord
- func (m *Manager) WaitChatIdle(ctx context.Context, agentID string) error
- func (m *Manager) WaitChatIdleSelfCall(ctx context.Context, agentID string) error
- func (m *Manager) WakeChat(agentID, message string) error
- func (m *Manager) WakeChatThread(agentID, sessionKey, msg string) error
- func (m *Manager) WakeThread(agentID, sessionKey, msg string) error
- func (m *Manager) WatchChatStart(agentID string) (<-chan struct{}, func())
- type MemoryEntryListOptions
- type MemoryEntryPatch
- type MemoryEntryRecord
- type MemoryIndex
- func (idx *MemoryIndex) BuildContextFromQuery(query string) string
- func (idx *MemoryIndex) ClearEmbeddings()
- func (idx *MemoryIndex) Close() error
- func (idx *MemoryIndex) IndexFiles(agentID string) error
- func (idx *MemoryIndex) IndexFilesIfStale(agentID string)
- func (idx *MemoryIndex) IndexMessages(agentID string) error
- func (idx *MemoryIndex) IndexNewMessages(agentID string)
- func (idx *MemoryIndex) Reindex(agentID string) error
- func (idx *MemoryIndex) Search(query string, limit int) ([]SearchResult, error)
- type MemoryRecord
- type Message
- type MessageAttachment
- type MessagePreview
- type MissingExpectedIDError
- type NotifyMode
- type OTPEntry
- type OneShotOpts
- type PersonaRecord
- type RateLimitInfo
- type RateLimitSnapshot
- type SearchResult
- type SkippedSessionFile
- type SlackBotConfig
- type StaleExpectedIDError
- type SteerFunc
- type TOTPParams
- type TTSConfig
- type Task
- type TaskCreateParams
- type TaskUpdateParams
- type ThreadLiveSnapshot
- type ToolUse
- type TruncateMemoryResult
- type Usage
Constants ¶
const ( InjectionUserContext = "user_context" // user.md block in the system prompt InjectionMemoryMD = "memory_md" // Current MEMORY.md injected block InjectionCredentials = "credentials" // credentials pointer + warnings InjectionGroupDM = "groupdm" // Group DM section (memberships + guide pointer) InjectionTodoAPI = "todo_api" // todo guide pointer + Active Todos in volatile context InjectionAttachments = "attachments" // attachment staging pointer InjectionStatus = "status" // Your Status section InjectionDiaryNotes = "diary_notes" // Recent Activity block in volatile context InjectionMemorySearch = "memory_search" // Relevant Memory block in volatile context InjectionRecentConversation = "recent_conversation" // session-resume transcript fallback InjectionPersonaAnchor = "persona_anchor" // persona anchor block appended after volatile context )
Context-injection section keys togglable via Agent.DisabledInjections.
const ( SteerModeInjected = "steer" SteerModeFallbackTurn = "fallback_turn" )
Steer injects an additional user message into the agent's currently running turn (claude/codex backends only — see ChatOptions.OnSteerReady / SteerFunc). It returns the mode used: SteerModeInjected when the text merged into the running turn, or SteerModeFallbackTurn when the turn had just ended and a normal follow-up turn was started with the text instead (so the message is never dropped on the turn-just-ended race — the reply arrives as a fresh turn). Returns ErrQuiescing during a restart drain (before any persistence), ErrSteerUnsupported if the backend cannot steer, and any real persist/backend failure otherwise. On the inject path the steered text is appended to the transcript as a plain user message; on the fallback path Chat persists it exactly once.
A steer that arrives while the turn is still starting — prepareChat (memory search, context assembly) runs BEFORE the busy entry exists, and OnSteerReady fires slightly after it — waits for the handle instead of bouncing 409 not_busy. Without the wait, a message sent in that window falls back to a queued normal send and gets reordered behind steers sent later in the same turn. Steer return modes (the first result value). SteerModeInjected means the text was merged into the running turn; SteerModeFallbackTurn means the turn had just ended and a normal follow-up turn was started with the text instead — the caller/UI should treat it like an ordinary send whose reply arrives as a fresh turn over the agent WS.
const CronPresetSentinelPrefix = "@preset:"
CronPresetSentinelPrefix is the marker for the "expand-with-per-agent- offset" sentinel form. The UI sends "@preset:30" when the user picks the "30m" preset chip; the manager resolves it via intervalToCronExpr at Save time so each agent gets a distinct minute-of-hour and stops the "all bots fire at :00" thundering-herd that fixed cron strings caused.
const DefaultCheckinContent = "If there are recent events or observations, record them in memory/{date}.md, and execute any necessary tasks."
DefaultCheckinContent is the prompt body used as the periodic-check-in fallback when an agent has no checkin.md. Surfaced as the pre-filled template by ReadCheckinFileOrDefault so what the settings UI shows and what cron / manual check-ins actually run agree. `{date}` expands to today's YYYY-MM-DD at runtime.
const ( // DefaultEmbeddingModel is the Gemini embedding model used when no model // has been explicitly configured. Exported so HTTP handlers can present // the same fallback without duplicating the string. DefaultEmbeddingModel = "gemini-embedding-001" )
const DefaultStatusContent = `{}
`
DefaultStatusContent is the initial status.json body seeded into new agent dirs (ensureAgentDir) and surfaced by the API / system prompt when an agent has no status row yet. Empty on purpose — status is freeform key-value JSON the agent writes about itself as it goes; starting empty avoids putting words (and default values) in the agent's mouth. writeStatusSection omits the whole "Your Status" section from the system prompt while the object stays empty.
const DefaultThreadName = "無題のスレッド"
DefaultThreadName is the placeholder title a new thread room carries until its first agent reply auto-titles it (see maybeAutoTitleThread).
const DefaultUserContent = `` /* 270-byte string literal not displayed */
DefaultUserContent is the template surfaced by the settings UI when an agent has no user.md yet. NOT written to disk until the user saves, so unfilled templates never reach the system prompt.
const ErrMsgCancelled = "cancelled: process was terminated"
ErrMsgCancelled is the error message attached to a "done" event when the backend process is terminated due to a user-initiated cancellation (context.Canceled) rather than a deadline.
const ErrMsgTimeout = "timeout: process was terminated"
ErrMsgTimeout is the error message attached to a "done" event when the backend process is terminated due to a context timeout.
const MaxConflictDiff = 50
MaxConflictDiff caps the number of messages returned to a caller whose expectedLatestMessageId is stale. Picked at 50 — same as the default GET messages page — so the conflict response stays small enough to inline in a single agent prompt while still covering normal traffic between two consecutive turns. When the diff exceeds the cap the caller is told (HasMore=true) to fetch the full transcript.
const MaxCronExprLen = 256
MaxCronExprLen caps the byte size of a stored cron expression. The 5-field standard form never needs anywhere near this many characters; the limit is purely a defence against pathological input getting persisted to agents.json or echoed back into log lines.
const MaxCronMessageRunes = 4096
MaxCronMessageRunes caps the per-agent cron check-in custom message at 4096 Unicode code points. The limit is in runes (not bytes) so Japanese users can write the same number of characters as ASCII users; the resulting byte size can be up to ~16 KiB worst case for 4-byte runes. The message is re-injected on every cron run so an unbounded value would inflate every prompt and the on-disk agent record.
const UserSenderID = "user"
UserSenderID is the reserved agent ID used for messages posted by the human user (operator) through the Web UI. It is never assigned to a real agent and is distinguished from agent senders by notifyState.senderIsUser.
const UserSenderName = "User"
UserSenderName is the display name recorded for user-authored messages.
Variables ¶
var ( ErrAvatarInternal = errors.New("cannot resolve temp dir") ErrAvatarNotFound = errors.New("file not found") ErrAvatarUnsupportedImage = errors.New("unsupported image format") )
Sentinel errors for ValidateTempAvatarPath, allowing callers to map to appropriate HTTP status codes.
var ( ErrAgentNotFound = errors.New("agent not found") ErrAgentBusy = errors.New("agent is busy") // ErrQuiescing is returned by the chat/mutation admission gates // (acquirePreparing / AcquireMutation / SetSwitching / acquireResetGuard) // while a daemon-wide restart drain is quiescing the manager. It wraps // ErrAgentBusy so existing errors.Is(err, ErrAgentBusy) checks (and the // HTTP 409 mapping) keep working, while errors.Is(err, ErrQuiescing) lets // callers distinguish "server is restarting" from a plain busy agent and // surface the dedicated "quiescing" error code. ErrQuiescing = fmt.Errorf("%w: server is restarting", ErrAgentBusy) ErrAgentResetting = errors.New("agent is being reset") ErrAgentArchived = errors.New("agent is archived") ErrGroupNotFound = errors.New("group not found") ErrGroupNotMember = errors.New("agent is not a member of group") ErrGroupTooFew = errors.New("group requires at least 2 members") ErrGroupAlreadyMember = errors.New("agent is already a member of group") ErrCredentialNotFound = errors.New("credential not found") ErrNoTOTPSecret = errors.New("no TOTP secret configured") ErrInvalidTOTP = errors.New("invalid TOTP parameters") ErrUnsupportedTool = errors.New("unsupported tool") ErrInvalidCronExpr = errors.New("invalid cron expression") ErrUnsupportedTimeout = errors.New("unsupported timeout") ErrInvalidRegenerate = errors.New("invalid regenerate target") // ErrAgentNotBusy is returned by Steer / SteerOneShot when there is no // turn currently running to steer. ErrAgentNotBusy = errors.New("agent has no turn in progress") // ErrQuestionNotFound is returned by Manager.AnswerQuestion when the // given requestID does not match any pending user_question on the // agent's running turn (already answered, expired, or never existed). ErrQuestionNotFound = errors.New("no pending question with that request id") )
Sentinel errors for the agent package.
var ErrHubStorageFailure = errors.New("hub-local settings storage failure")
ErrHubStorageFailure marks a non-not-found store read/write failure on the hub-local settings path; mapped to 500 (not 404/400).
var ErrInvalidMemoryEntry = errors.New("invalid memory entry")
ErrInvalidMemoryEntry is the sentinel for client-supplied data that fails validation (bad kind, malformed name, oversized body). Maps to HTTP 400 at the handler. Operational failures (DB errors, file I/O errors, sync errors) do NOT wrap this — they fall through to 500 so the caller can distinguish their bug from ours.
var ErrInvalidPersona = errors.New("invalid persona")
ErrInvalidPersona is the sentinel for client-supplied persona data that fails validation (oversized body). Maps to HTTP 400.
var ErrInvalidTaskStatus = errors.New("invalid task status")
ErrInvalidTaskStatus is returned for status values that aren't part of the v0 vocabulary the public API accepts.
var ErrMemoryEntryExists = errors.New("memory entry already exists")
ErrMemoryEntryExists is returned when CreateAgentMemoryEntry would collide with an existing live row under the same (kind, name). Maps to HTTP 409 at the handler.
var ErrMemoryEntryNotCanonical = errors.New("memory entry is at a non-canonical path; delete and recreate")
ErrMemoryEntryNotCanonical signals an attempt to UPDATE a row whose backing file lives at a non-canonical path (e.g. legacy fall-through `memory/foo.md` for a kind=topic row). PATCH would silently mint a new file at the canonical path and orphan the old one — which the next sync would resurrect as a duplicate row. Caller should DELETE the legacy entry and CREATE it fresh under the canonical layout. Maps to HTTP 409.
var ErrMemoryEntryRenameUnsupported = errors.New("rename via PATCH is not supported; use DELETE + CREATE")
ErrMemoryEntryRenameUnsupported is returned by UpdateAgentMemoryEntry when the patch attempts to change kind or name. Rename via PATCH is genuinely hard to make crash-safe without an intent-file protocol — callers should DELETE + CREATE instead. Maps to HTTP 400.
var ErrMemoryEntryStoredCorrupt = errors.New("stored memory entry data is invalid")
ErrMemoryEntryStoredCorrupt signals a DB row whose persisted kind or name fails validation when read back (peer-replicated junk, an importer bug, manual SQL surgery). It is NOT a client error — the request is well-formed; the server has a bad row. Maps to HTTP 500 so monitoring catches it.
var ErrMessageETagMismatch = errors.New("message etag mismatch")
ErrMessageETagMismatch is returned when an If-Match precondition on a message-mutation API does not match the current row's etag. Maps to HTTP 412 at the handler layer (RFC 7232).
var ErrMessageNotFound = errors.New("message not found")
ErrMessageNotFound is returned when a message with the given ID does not exist.
var ErrOverrideRecordFailed = errors.New("hub-local override record failed; retry the request")
ErrOverrideRecordFailed marks a PATCH whose pending-override kv bookkeeping could not be persisted. Surfaced as 500; safe to retry (phase-1 failure wrote nothing, phase-3 failure is lazily healed by the retry).
var ErrSteerUnsupported = errors.New("steering is not supported for this backend")
ErrSteerUnsupported is returned by Manager.Steer / Manager.SteerOneShot when the target turn is running on a backend that doesn't support mid-turn steering (claude and codex do today).
var ErrTaskETagMismatch = errors.New("task etag mismatch")
ErrTaskETagMismatch is returned by Update/Delete when a non-empty If-Match assertion didn't match the current row's etag.
var ErrTaskNotFound = errors.New("task not found")
ErrTaskNotFound is returned by Manager task helpers when the requested row is missing or tombstoned. Distinct from other errors so handlers can map cleanly to 404.
var ErrTaskTitleEmpty = errors.New("title cannot be empty")
ErrTaskTitleEmpty is returned when UpdateTask receives an explicit title that's empty after trimming. (TaskUpdateParams.Title is a pointer, so nil = leave alone; a non-nil pointer to empty string is caller intent to clear the title, which we refuse since title is required.)
var ErrTaskTitleRequired = errors.New("title is required")
ErrTaskTitleRequired is returned when CreateTask receives an empty or whitespace-only title.
var ValidGroupDMStyles = map[GroupDMStyle]bool{ GroupDMStyleEfficient: true, GroupDMStyleExpressive: true, }
ValidGroupDMStyles is the set of accepted style values.
var ValidGroupDMVenues = map[GroupDMVenue]bool{ GroupDMVenueChatroom: true, GroupDMVenueColocated: true, }
ValidGroupDMVenues is the set of accepted venue values.
var ValidNotifyModes = map[NotifyMode]bool{ NotifyRealtime: true, NotifyDigest: true, NotifyMuted: true, }
ValidNotifyModes is the set of accepted notify-mode values.
Functions ¶
func AgentDir ¶ added in v0.10.0
AgentDir returns the data directory path for the given agent ID. Exported for use by external subsystems (e.g. slackbot Hub) that need to resolve agent data directories without importing agent internals.
func BuildMCPServers ¶ added in v0.12.0
BuildMCPServers returns the set of MCP servers that should be available to the given agent. apiBase is the kojo server URL (e.g. "http://127.0.0.1:8080").
When the agent has its own kojo auth token (i.e. agentTokenLookup is wired up by the server) the per-agent /mcp endpoint requires the X-Kojo-Token header. Without it the call lands as a Guest principal and is denied by the auth middleware.
func DefaultAgentWorkDir ¶ added in v0.101.0
DefaultAgentWorkDir returns the portable per-peer agent work directory used when an §3.7 sync arrives without a workDir suitable for the local platform. Format: `<userhome>/.kojo/agent-workspaces/<agent_id>`. Resolves the home dir via os.UserHomeDir so $HOME (macOS / Linux) or %USERPROFILE% (Windows) is honored. Falls back to the kojo AgentDir as a last resort if home is unavailable.
func DeleteAvatar ¶ added in v0.101.0
DeleteAvatar removes every published avatar.* blob for an agent. Used by the reset and delete paths so a re-created agent sharing the same id starts with no avatar. ErrNotFound on individual extensions is folded into success — the post-condition is "no avatar blobs for this agent", which is already true for any extension that wasn't published.
Acquires the per-agent avatar lock so a concurrent SaveAvatar can't observe a half-deleted state (some extensions cleaned, the new Put landed). See SaveAvatar for the rationale.
func EnsureAgentWorkspaceDirIfDefault ¶ added in v0.101.0
EnsureAgentWorkspaceDirIfDefault MkdirAll's workDir when it matches DefaultAgentWorkDir(agentID), and otherwise is a no-op. Used by the peer sync handler and the agent create/update validators so the portable default path doesn't trip the "workDir does not exist" check on the first save after a §3.7 device switch — only kojo itself can produce that exact path, so auto-creating it doesn't expand the surface for arbitrary attacker-supplied paths. Non-matching workDir values still go through the normal os.Stat existence check at the call site.
Hardening: agentID is gated through agentIDDirPattern before it ever reaches filepath.Join, so a peer-sync payload claiming an id of `../../etc` can't steer MkdirAll outside the agent-workspaces subtree. The MkdirAll target is the cleaned default path (not the caller-supplied workDir) — equality on cleaned paths guarantees the two resolve identically, and using the canonical form keeps a `.../foo/.` style input from leaving a trailing-dot artifact on disk.
func GenerateAvatarWithAI ¶
func GenerateAvatarWithAI(ctx context.Context, agentID string, persona string, name string, prompt string, logger *slog.Logger) (string, error)
GenerateAvatarWithAI generates an avatar by calling the Gemini image generation API directly. Returns the path to an image file inside a kojo-avatar-* temp dir.
func GenerateName ¶
GenerateName generates a character name based on persona description.
func GeneratePersona ¶
GeneratePersona elaborates or refines a persona description. currentPersona may be empty (generate from scratch) or non-empty (refine existing).
func GeneratePublicProfile ¶
GeneratePublicProfile creates a short outward-facing description from a persona.
func GenerateSVGAvatarFile ¶
GenerateSVGAvatarFile creates an SVG avatar file in a temp directory and returns its path. Used as fallback when AI avatar generation is unavailable.
func GenerateTOTPCode ¶
GenerateTOTPCode generates a current TOTP code for the given secret and parameters.
func GuideDir ¶ added in v0.111.0
func GuideDir() string
GuideDir returns the shared guide directory under the kojo config root. Not per-agent: every agent reads the same files.
func IsAllowedImageExt ¶ added in v0.7.0
IsAllowedImageExt returns true if ext (case-insensitive) is an accepted avatar image extension.
func IsHubLocalSafePatchKey ¶ added in v0.111.0
IsHubLocalSafePatchKey reports whether a top-level PATCH body key is classified hub-local-safe. Matching is case-insensitive to mirror encoding/json's field matching (and the privileged / disabledInjections casing guards in handleUpdateAgent). Callers must lower-case the key.
func IsInSilentHours ¶ added in v0.18.0
IsInSilentHours checks if the current local time is within the silent window. Returns false if no restriction is set (both empty = never silent). Supports overnight ranges (e.g., 23:00-09:00).
func IsPathSafeAgentID ¶ added in v0.101.0
IsPathSafeAgentID reports whether id is safe to embed in an on-disk path or filename. Exported so HTTP handlers can reject peer-sync payloads with a malformed agent.id at the boundary, returning 400 instead of letting the value flow to filepath.Join.
func LoadGeminiAPIKey ¶ added in v0.19.0
func LoadGeminiAPIKey(creds *CredentialStore) (string, error)
LoadGeminiAPIKey is the exported wrapper around loadGeminiAPIKey for callers outside the agent package (e.g. internal/server's TTS handler). It applies the same priority order: encrypted credential store first, then the legacy nanobanana credentials file as a fallback.
func LoadXAIAPIKey ¶ added in v0.115.0
func LoadXAIAPIKey(creds *CredentialStore) (string, error)
LoadXAIAPIKey loads the xAI (Grok) API key for callers outside the agent package (internal/server's STT ephemeral-token handler). Priority: 1) encrypted credential store (provider "xai"), 2) XAI_API_KEY env var, 3) the grok-research credentials file.
func LockAgentMemorySync ¶ added in v0.101.0
func LockAgentMemorySync(agentID string) func()
LockAgentMemorySync acquires the per-agent memorySyncMu and returns the release callback. Exported so the §3.7 agent-sync HTTP handler can hold the lock across BOTH the DB write (store.SyncAgentFromPeer) AND the disk materialize step — otherwise a concurrent prepareChat could grab the lock between the commit and our materialize, walk stale disk, and UPSERT yesterday's bodies back into the DB the sync just refreshed.
Caller MUST defer the returned func. Reentrant use within the same goroutine deadlocks (sync.Mutex is not reentrant); the matching MaterializeAgentSyncToDiskHeld variant assumes the lock is already held precisely so this stays clean.
func LookupAgentToken ¶ added in v0.101.0
LookupAgentToken returns the raw $KOJO_AGENT_TOKEN value for the given agent_id via the wired callback. Returns ("", false) when the callback is unset or the agent has no available raw value (e.g. post-restart on a peer that only holds the kv hash, not the raw — see auth.ErrTokenRawUnavailable). Used by the §3.7 orchestrator to capture the source's raw so it can be replayed to target's TokenStore via /api/v1/peers/agent-sync.
func LookupPeerCount ¶ added in v0.101.0
func LookupPeerCount() int
LookupPeerCount returns the number of OTHER registered peers via the wired callback. Returns 0 when no callback is set.
func NormalizeTOTPSecret ¶
NormalizeTOTPSecret normalizes and validates a TOTP secret. Returns the canonical form (upper-case, no padding) or an error.
func NormalizeThinkingMode ¶ added in v0.10.1
NormalizeThinkingMode normalizes "auto" to "" for storage.
func ParseCronSchedule ¶ added in v0.101.0
ParseCronSchedule parses an expression into a cron.Schedule using the same strict parser as ValidateCronExpr. Empty input is an error here — callers that want to allow "disabled" must check upfront.
func PreCompactSummarize ¶ added in v0.9.0
func PreCompactSummarize(agentID string, tool string, transcriptPath string, logger *slog.Logger) error
PreCompactSummarize is called by the PreCompact hook (via API) just before Claude Code compacts the conversation. It reads from Claude's live session JSONL (which contains the full current context including pending tool uses) rather than kojo's persisted transcript (agent_messages, which may lag behind). Falls back to the agent_messages transcript via loadMessages if the session JSONL is unavailable.
transcriptPath, when non-empty, is the JSONL path that Claude's PreCompact hook supplied via stdin. It is validated to live under the agent's claude project directory before opening — the path is hook-supplied and must not be trusted blindly. On validation failure we silently fall back to project-dir discovery; we don't propagate the error because legitimate older claude builds may not populate the field.
Two guards short-circuit the work before any LLM call:
- The "no messages" case (nothing happened yet).
- The fingerprint check — if the last preCompactMaxMessages haven't changed since the previous summary, there's nothing new to record. The check is content-based (md5 of the stripped messages), not time-based: under PreCompact storms each fire usually carries new tool_use / tool_result content, and a time-only skip would lose exactly the short-term context we're trying to preserve. Per-agent serialisation (preCompactLocks) is what actually collapses concurrent fires.
Successful summaries update the marker, append to the daily diary, and atomically rewrite memory/recent.md (the canonical file the per-turn volatile context reads from). All three writes are serialised under a per-agent lock to prevent concurrent fires from racing on the marker or the recent.md tempfile.
func PrepareClaudeSettings ¶ added in v0.9.0
func PrepareClaudeSettings(agentID, apiBase string, allowProtectedPaths []string, logger *slog.Logger)
PrepareClaudeSettings writes .claude/settings.local.json with persona override, the bash-capture PreToolUse hook (always installed), and (when apiBase is available) a PreCompact hook that calls kojo's API to save a conversation summary before Claude compacts context. Called from Manager.Chat before backend.Chat to ensure settings are in place before the Claude process reads them.
func ReadCheckinFileOrDefault ¶ added in v0.20.0
ReadCheckinFileOrDefault reads checkin.md and falls back to DefaultCheckinContent when the file is absent. Used by the API so the UI shows a template for agents that haven't configured a custom check-in yet. Returns (content, isDefault, err).
Empty / whitespace-only checkin.md is treated as absent here so the UI and the cron/manual prompt agree even if a user manually placed a blank file. Other I/O errors are surfaced so the API responds with 500 instead of silently masking the failure.
func ReadClaudeSessionFiles ¶ added in v0.101.0
func ReadClaudeSessionFiles(agentID string) ([]ClaudeSessionFile, []SkippedSessionFile, error)
ReadClaudeSessionFiles pulls every session JSONL claude has recorded for the given agent's source AgentDir. Returns an empty slice (no error) if the project dir doesn't exist — agents that have never started a claude conversation have no state to migrate.
File-content size has a per-file ceiling (claudeSessionMaxBytes) so a runaway log file can't blow up the agent-sync payload. Files larger than the ceiling are skipped and recorded in the returned skipped slice (path + reason + size) so the loss can be surfaced to the agent / operator / owner instead of only warn- logged.
func ReadCodexSessionFiles ¶ added in v0.101.5
func ReadCodexSessionFiles(agentID string) (*CodexSessionTransfer, []SkippedSessionFile, error)
func ReadGrokSessionFiles ¶ added in v0.101.0
func ReadGrokSessionFiles(agentID string) (*GrokSessionTransfer, []SkippedSessionFile, error)
ReadGrokSessionFiles collects the agent's PRIMARY grok session for handoff. Returns (nil, nil, nil) when there is nothing to migrate — no `.grok/session_id` file, an unreadable / malformed session id, or an empty session directory. Those are NOT errors: a fresh agent or a non-grok agent simply has no grok state to move, and we want the orchestrator to ship a claude payload (if any) without failing the switch.
File-content size has a per-file ceiling. Oversized non-core files are skipped with their RelPath captured in the returned skipped slice (caller logs a warning). Oversized CORE files (events.jsonl, chat_history.jsonl, summary.json, system_prompt.txt) abort the read with an error — shipping a session without those would let target resume into a broken state worse than starting fresh.
Security: the session_id is validated against isGrokSessionID BEFORE it ever reaches filepath.Join, so a poisoned `.grok/session_id` (the agent can write its own workspace) cannot escape the session subtree.
func ReadUserFileOrDefault ¶ added in v0.20.0
ReadUserFileOrDefault returns user.md content, falling back to DefaultUserContent when the file does not exist. Used by the API so the UI shows the fill-in template for agents that haven't configured user context yet, without persisting the template to disk.
Returns (content, isDefault, err). isDefault=true means the caller is seeing the in-memory template (no user.md on disk), so the UI can avoid PUT-ing the template back to disk on a no-op save. Only os.IsNotExist triggers the default fallback; other I/O errors are surfaced so the API layer responds with 500 instead of masking the failure.
func ReadWorkspaceFile ¶ added in v0.101.0
func ReadWorkspaceFile(ctx context.Context, st *store.Store, agentID string, kind store.WorkspaceFileKind) (body string, isDefault bool, etag string, err error)
ReadWorkspaceFile reads the workspace file row from the DB. Returns (body, isDefault, etag, err):
- row exists → (rec.Body, false, rec.ETag, nil)
- ErrNotFound → (defaultTemplate, true, "", nil)
- other → ("", false, "", err)
Used by REST handlers that need to surface a pre-filled template for agents that have never written the file. The disk mirror is NOT consulted here — the DB is canonical and reconcile keeps disk in sync — so the response is identical across peers.
func RecentDiarySummary ¶ added in v0.9.0
RecentDiarySummary returns the latest pre-compaction summary for per-turn volatile-context injection. Returns empty string when no summary has been generated yet.
Source preference:
- memory/recent.md — the rolling, single-summary file rewritten on every successful PreCompactSummarize. This is the canonical short- term memory file. Bounded size, no day-boundary problem.
- memory/YYYY-MM-DD.md — today's append-only diary. Fallback for legacy agents that haven't generated a summary since recent.md was introduced.
Content is wrapped in a `<diary-notes>` block so the agent recognises it as data, not instructions.
func ReconcileAgentDiskFromDB ¶ added in v0.101.0
func ReconcileAgentDiskFromDB(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
ReconcileAgentDiskFromDB is the convenience wrapper that acquires memorySyncMu internally. Suitable for callers (tests, ad-hoc tools) that don't already hold the lock. The §3.7 agent-sync handler holds the lock across BOTH SyncAgentFromPeer and the reconcile — use ReconcileAgentDiskFromDBHeld there.
func ReconcileAgentDiskFromDBHeld ¶ added in v0.101.0
func ReconcileAgentDiskFromDBHeld(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
ReconcileAgentDiskFromDBHeld rewrites the agent's MEMORY.md and memory/* tree to match the AUTHORITATIVE post-commit DB state. Caller MUST hold the per-agent memorySyncMu (via LockAgentMemorySync) so the read-and-write sequence here can't race a concurrent disk→DB sync.
Reads DB state instead of the wire payload because:
Incremental mode ships only the delta — pre-existing stale disk for non-delta rows would not be healed by a delta-only materializer (the bug source for the original peer→hub diary-rollback report). Reading the live DB set covers every row, not just the changed ones.
The DB already filtered through schema CHECK + insert validation, so we don't have to defensively skip "kind/name looks bad" rows from the wire payload that wouldn't have made it into the DB anyway. The containment check stays as a paranoid last-line defense.
SyncAgentFromPeer is the only writer between us and the lock release, and it just committed — so reading from the DB reflects exactly the state we want disk to mirror.
Strategy per surface:
MEMORY.md: write if DB row is live and disk body differs; remove if DB has no live row (covers nil-from-source AND tombstoned-on-source).
memory_entries: list every live row; for each, write its body to the canonical disk path (skip if disk sha matches to avoid unnecessary I/O). Then scan disk and remove any *.md whose (kind, name) isn't in the live set (drops tombstoned rows + any orphan file source no longer has).
containmentCheck failures and other per-entry write errors are surfaced via the returned error — silently skipping a corrupt row would leave DB and disk inconsistent and could mask a real bug or attack attempt. The handler maps a non-nil return to HTTP 500 so the orchestrator retries.
func ReconcileWorkspaceFilesDiskFromDBHeld ¶ added in v0.101.0
func ReconcileWorkspaceFilesDiskFromDBHeld(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
ReconcileWorkspaceFilesDiskFromDBHeld rewrites the on-disk mirror of every workspace-file kind to match the authoritative DB state. The caller MUST hold the per-agent memorySyncMu via LockAgentMemorySync so the read-and-write sequence here can't race the disk→DB path.
Strategy per kind:
- live DB row → write the body to disk iff the existing disk file differs (sha mismatch) or is absent.
- no DB row (ErrNotFound) → ensure disk file is absent.
- other DB error → log and continue with the next kind so a transient failure on one kind doesn't strand the others.
Errors are surfaced via the returned error (first encountered); per-kind failures don't abort the loop so a single permission glitch on user.md doesn't block checkin.md from reconciling.
func ResolveCronPreset ¶ added in v0.101.0
ResolveCronPreset returns the per-agent-offset cron expression for a "@preset:N" sentinel, or the input unchanged for any other (validated) expression. Used at Save time so the persisted form is always a real 5-field cron string — the sentinel never reaches the cron scheduler.
Returns "" for an unknown / out-of-whitelist N so the caller can surface the schedule as disabled rather than picking a wrong cadence.
func SaveAvatar ¶
SaveAvatar publishes an uploaded avatar to the blob store. ext is the leading-dot extension ("." + e.g. "png"); callers are responsible for validating it via IsAllowedImageExt before calling.
Removes any pre-existing avatar at a different extension so the agent presents exactly one avatar at a time — without this a user who first uploads avatar.png and then avatar.svg would have BOTH surface, with resolveAvatarBlob's probe order picking .png and silently discarding the new svg. Matches the v0 disk-write semantics: SaveAvatar deletes every avatar.* before writing the new one.
Per-agent serialization (acquireAvatarLock) ensures the "delete-then-put" sequence is observed atomically — without it, two concurrent uploads at different extensions could interleave their delete/put calls and leave multiple rows in place.
Failure posture: if ANY of the per-extension cleanup Deletes fails (other than ErrNotFound), SaveAvatar aborts before the final Put. This is intentional — a partial cleanup that proceeds to Put could leave two avatars surviving and the wrong one wins resolveAvatarBlob's probe order. Aborting forces the operator to see and resolve the underlying store error.
func ServeAvatar ¶
ServeAvatar serves the agent's avatar image, falling back to a generated SVG when no avatar is published or the blob store hasn't been wired (test fixture). Uses http.ServeContent so conditional GET (If-Modified-Since / If-None-Match) and Range requests work the same way they did in the v0 http.ServeFile path.
Content-Type is inferred from the file extension; svg is special- cased because Go's mime package returns "image/svg+xml" but only when the system mime database has been initialized, which can't be relied on across all deploy targets.
func SetAgentTokenLookup ¶ added in v0.17.0
SetAgentTokenLookup wires the token lookup callback. May be nil (disables token injection).
func SetKojoAPIBase ¶ added in v0.17.0
func SetKojoAPIBase(base string)
SetKojoAPIBase records the URL agents should use for self-authenticated API calls. Idempotent; safe to call repeatedly during boot.
func SetPeerCountLookup ¶ added in v0.101.0
func SetPeerCountLookup(fn func() int)
SetPeerCountLookup wires the peer-count callback. May be nil (disables the device-switch skill auto-install).
func StageClaudeSessionFiles ¶ added in v0.101.0
func StageClaudeSessionFiles(agentID string, files []ClaudeSessionFile) (commit func(), rollback func(), err error)
StageClaudeSessionFiles materialises the source-captured JSONLs into target's claude project dir, computed from target's own AgentDir. The encoded path differs from source's (AgentDir is machine-local) but the per-file session_id is preserved so `claude --continue` finds the conversation it was running on the source peer.
It is a two-phase (stage/commit) operation: it writes the JSONLs to their final paths (with backups of any pre-existing files held aside) and returns commit/rollback callbacks. commit() drops the backups — the new content is the canonical state. rollback() restores the backups — the agent's pre-sync session files are intact.
The §3.7 agent-sync handler uses this so a DB sync failure AFTER the JSONL stage doesn't strand target with overwritten session files; the rollback restores the previous state.
commit / rollback are nil-safe and idempotent (calling either twice is a no-op). Both are nil when files is empty.
func StageCodexSession ¶ added in v0.101.5
func StageCodexSession(agentID string, transfer *CodexSessionTransfer) (commit func(), rollback func(), err error)
func StageCodexSessionCleanup ¶ added in v0.101.5
func StageGrokSession ¶ added in v0.101.0
func StageGrokSession(agentID string, transfer *GrokSessionTransfer) (commit func(), rollback func(), err error)
StageGrokSession materialises the source-captured grok session directory under target's own AgentDir-derived encoded path, plus the `<agentDir>/.grok/session_id` resume pointer.
Layout produced on target:
$GROK_HOME/sessions/<encoded(target absAgentDir)>/<sessionID>/<files...> <target agentDir>/.grok/session_id ← contains sessionID
The encoded path differs from source's because AgentDir is machine-local; the session UUID stays the same so backend_grok.go on target finds the migrated state via `--resume <sessionID>`.
Implementation strategy is DIRECTORY SWAP, not per-file overwrite: every transferred file is staged into a fresh sibling `<sessionRoot>.staging-*` directory; on commit we rename the pre-existing `<sessionRoot>` (if any) to `<sessionRoot>.backup-*`, then rename `<sessionRoot>.staging-*` to `<sessionRoot>`. The resume pointer is staged separately. commit drops both backups; rollback restores them.
Why directory swap instead of per-file overwrite: target may be holding stale files from a previous handoff (e.g. an aborted turn that wrote `events.jsonl.partial`). A naive per-file overwrite would leave those orphans on disk, and grok's `--resume` would happily pick them up. Swap guarantees target's post-commit `<sessionRoot>` contains EXACTLY the files source shipped.
commit / rollback are nil-safe and idempotent. Both return nil-nil-nil when transfer is nil / empty — the caller treats that as "no grok state to migrate" (and may follow up with StageGrokSessionCleanup if it also wants target's stale state purged).
func StageGrokSessionCleanup ¶ added in v0.101.0
StageGrokSessionCleanup purges any pre-existing primary grok session state from target's agentDir. Used by the agent-sync handler when the inbound payload says the agent IS a grok agent but carries NO GrokSession block — i.e. source either has no session yet OR cleared it via ResetSession. Without this purge, target would keep the stale `.grok/session_id` file it inherited from a previous switch (or wrote during a local turn while it hosted the agent earlier) and the next chat would `--resume` that local UUID instead of starting fresh, presenting the user with a conversation that bears no relation to source's current state.
Same two-phase contract as StageGrokSession: stages the deletion by RENAMING the pointer + session root to backups, returns commit / rollback callbacks. commit() drops the backups (the purge becomes canonical). rollback() restores them (state stays as it was).
commit / rollback are nil-safe and idempotent. Returns (nil, nil, nil) when there is nothing to purge.
func SyncAgentMemoryFromDisk ¶ added in v0.101.0
func SyncAgentMemoryFromDisk(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
SyncAgentMemoryFromDisk runs both MEMORY.md and memory/ syncs for one agent. Used by Manager.Load (startup), Manager.fork (post-copy), and any other code that knows the disk state may have diverged from the DB.
Per-agent serialization: holds memorySyncMu[agentID] for the entire run so concurrent callers (fork + prepareChat hitting the same agent, startup overlapping with an in-flight Web UI write, etc.) don't race their FS reads against each other's DB writes.
Best-effort: each sub-sync logs its own errors and the function returns the first encountered so the caller can log a summary. Both sub-syncs run regardless of one's failure — a corrupted MEMORY.md should not prevent memory/ entries from syncing.
func SyncAgentMemoryFromDiskBestEffort ¶ added in v0.101.0
func SyncAgentMemoryFromDiskBestEffort(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) (ran bool, err error)
SyncAgentMemoryFromDiskBestEffort is the non-blocking variant for hot paths (prepareChat). If the per-agent mutex is held by another sync in flight, we skip this turn rather than block — the file remains canonical for the CLI prompt build that runs immediately after, and the next prepareChat / Load / scheduled hook will retry. Returns (false, nil) on skip; (true, syncErr) when the sync ran.
The DB context is internal: a fresh 5s timeout rooted at context.Background(). The caller's ctx is intentionally NOT propagated (see dbContextWithCancel for why) — its deadline would otherwise reapply to the DB ops, which is exactly the foot-gun the detachment is meant to avoid.
func SyncAgentPersonaFromDisk ¶ added in v0.101.3
func SyncAgentPersonaFromDisk(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
SyncAgentPersonaFromDisk reconciles the on-disk persona.md with the agent_persona DB row. It is the pure disk→DB half of Manager.syncPersona — WITHOUT the in-memory Agent.Persona update, the publicProfile regeneration, or the AcquireMutation device-switch gate.
The gate omission is deliberate: this function is called from the device-switch source-flush (switch_device_handler step 0-pre) AFTER SetSwitching(true) is already set. Manager.syncPersona refuses to run in that window (AcquireMutation returns an error once switching is set), so the closing turn's persona.md edit would otherwise never reach the DB row that buildAgentSyncRequest ships to the target — silently rolling the persona back on the destination peer.
Semantics mirror syncAgentMemoryToDB (DB-canonical, disk mirror):
- file present, body differs from DB → UPSERT disk→DB (disk wins)
- file present, body matches DB → no-op
- file missing + live non-empty DB row → HYDRATE disk from DB
- file missing + no/empty/tombstoned row → no-op
A missing file is NEVER treated as a clear: clearing persona must round-trip through PutAgentPersona / Manager.Update so an operator `rm persona.md` cannot wipe the canonical row. Idempotent and safe to call from any known mutation hook.
Locking: holds personaSyncMu(agentID) for the whole read→write critical section so a concurrent PutAgentPersona can't interleave. It takes ONLY personaSyncMu (never memorySyncMu), so it cannot invert the memorySyncMu→personaSyncMu order and deadlock.
func SyncAttachSkill ¶ added in v0.101.0
SyncAttachSkill writes or removes the kojo-attach SKILL.md based on `enabled`. Idempotent: safe to call on every prepareChat. Failures are logged at warn level; the agent can still run without the skill so an I/O error here must not block the chat.
Skill placement mirrors SyncDeviceSwitchSkill: per-agent under <agentDir>/.claude/skills/kojo-attach/SKILL.md. The claude CLI walks up from its cwd looking for skills/, and we set cmd.Dir to agentDir when spawning, so the skill is in scope.
func SyncAttachSkillForTool ¶ added in v0.101.5
func SyncCodexAttachSkill ¶ added in v0.101.5
SyncCodexAttachSkill installs the same kojo-attach instructions in Codex's project skill tree. Codex app-server loads `.codex/skills` from cwd, while Claude/Grok use `.claude/skills`.
func SyncCodexDeviceSwitchSkill ¶ added in v0.101.5
SyncCodexDeviceSwitchSkill installs Codex's project-scoped skill under `.codex/skills`. The body follows the grok-compatible no-inline-exec shape because Codex skills do not support Claude Code's `!`...“ substitution syntax.
func SyncDeviceSwitchSkill ¶ added in v0.101.0
SyncDeviceSwitchSkill writes or removes the kojo-switch-device SKILL.md based on (enabled && peerCountLookup() > 0). Idempotent: safe to call on every prepareChat. Failures are logged at warn level; the agent can still run without the skill, so an I/O error here must not block the chat.
The caller passes the toggle explicitly (typically via agent.IsDeviceSwitchEnabled()); peer count is read from the package-level callback wired in cmd/kojo/main.go.
On Windows, a cmd.exe / PowerShell-compatible body is installed (deviceSwitchSkillBodyWindows) using curl.exe — shipped in C:\Windows\System32 since Windows 10 1803. POSIX body stays for macOS / Linux.
func SyncDeviceSwitchSkillForTool ¶ added in v0.101.0
SyncDeviceSwitchSkillForTool is the backend-aware entry point that callers (prepareChat, peer arrival, Update) should use instead of the lower-level Sync*DeviceSwitchSkill variants. Dispatches on the agent's current Tool value:
"claude" / "custom": install the Claude-Code body when enabled, remove otherwise (SyncDeviceSwitchSkill).
"grok": install the grok-flavored body when enabled, remove otherwise (SyncGrokDeviceSwitchSkill). The grok body avoids Claude-Code-only `!`exec“ substitution and mentions `grok --resume` instead of `claude --continue`. Writing OVER any pre-existing claude-body SKILL.md is intentional: a tool change must yield a body the new backend can execute.
"codex": install the codex-flavored body under `.codex/skills` when enabled, remove otherwise (SyncCodexDeviceSwitchSkill).
any other tool (llama.cpp): no-op.
func SyncGrokDeviceSwitchSkill ¶ added in v0.101.0
SyncGrokDeviceSwitchSkill is the grok-flavored counterpart to SyncDeviceSwitchSkill. Uses the same lock + atomic-write machinery but writes the deviceSwitchSkillBodyGrok* variant so the body no longer relies on Claude-Code-only `!`...“ inline shell substitution and mentions `grok --resume` instead of `claude --continue`. Same enable + peer-count gate.
func SyncGuides ¶ added in v0.111.0
SyncGuides writes the embedded guide files to GuideDir(), overwriting a file only when its content differs from the embedded copy (so an upgraded binary refreshes stale guides while steady-state boots are write-free). Failures are logged and non-fatal: the agent can still run, it just gets a dangling pointer until the next sync succeeds. Returns true only when every guide file was verified/written — the throttle uses this to decide whether to retry on the next call.
func SyncWorkspaceFilesFromDisk ¶ added in v0.101.0
func SyncWorkspaceFilesFromDisk(ctx context.Context, st *store.Store, agentID string, logger *slog.Logger) error
SyncWorkspaceFilesFromDisk reconciles the on-disk workspace files into the agent_workspace_files table. Reverse direction of ReconcileWorkspaceFilesDiskFromDBHeld — used by Load and any other path that suspects a CLI-direct edit of user.md / checkin.md happened while the daemon was down.
For each kind:
- disk file absent: SoftDeleteAgentWorkspaceFile (idempotent unconditional). On Load this is harmless when the row was never live; in steady state it catches "user removed checkin.md in the CLI" propagation.
- disk file present:
- DB body matches → skip
- DB body differs / no row → Upsert with AllowOverwrite=true
Best-effort: per-kind failures log and surface as the returned firstErr but don't short-circuit other kinds.
func TurnSummarize ¶ added in v0.111.0
TurnSummarize runs the incremental summarization after a completed chat turn, when enough un-summarized backlog has accumulated. This is the proactive sibling of the PreCompact hook: instead of waiting for the 150k-token compaction fire (and racing its chunk budget against session deletion), the backlog is worked down a turn at a time, so memory/recent.md stays close to the live conversation and the eventual pre-compaction fire has little left to do.
The gate is deliberately cheap and lock-free: it streams the session JSONL, strips volatile context, and measures the content bytes past the marker cursor. Below turnSummaryMinBacklogBytes it returns nil without any LLM call. At or above, it delegates to PreCompactSummarize, which re-reads state under the per-agent lock — a concurrent fire that advanced the cursor in between simply shrinks (or empties) the backlog there, so the racy read here can cause at most one redundant no-op pass, never a duplicate summary.
Only the claude tool is supported (the session JSONL is the cursor's substrate); other tools return nil immediately. The agent's MAIN session file (deterministic UUID from the agent ID) is targeted explicitly — mtime-based discovery could otherwise pick a concurrent Slack/Discord thread's JSONL and summarize the wrong conversation. One-shot / session-key threads are deliberately not summarized here.
func ValidEffort ¶ added in v0.8.0
ValidEffort returns true if the given effort level is valid.
func ValidInjectionKey ¶ added in v0.111.0
ValidInjectionKey reports whether key is a known injection section.
func ValidModelEffort ¶ added in v0.10.1
ValidModelEffort returns true if the model+effort combination is valid. xhigh is only allowed for specific models.
func ValidResumeIdle ¶ added in v0.15.0
ValidResumeIdle returns true if the given resume-idle window is in the allowed set.
func ValidSilentHours ¶ added in v0.18.0
ValidSilentHours validates the silent hours range. Both must be empty (no restriction) or both must be valid HH:MM format.
func ValidThinkingMode ¶ added in v0.10.1
ValidThinkingMode returns true if the given thinking mode is valid.
func ValidTimeout ¶ added in v0.10.0
ValidTimeout returns true if the given timeout is in the allowed set.
func ValidateCronExpr ¶ added in v0.101.0
ValidateCronExpr returns nil for an empty expression (= scheduling disabled), for a valid "@preset:N" sentinel (N must be in legacyAllowedIntervals), or for a valid 5-field cron expression. Anything else is rejected with a wrapped parse error.
func ValidateDisabledInjections ¶ added in v0.111.0
ValidateDisabledInjections rejects unknown keys so a typo'd PATCH fails loudly instead of silently never disabling anything.
func ValidateTOTPParams ¶
ValidateTOTPParams validates TOTP parameters and normalizes the secret. Returns the normalized secret or an error.
func ValidateTTS ¶ added in v0.19.0
ValidateTTS checks a TTSConfig against the canonical model/voice allow-lists. Empty Model/Voice are accepted (treated as "use default" at synthesize time). StylePrompt is length-clipped, not validated.
func ValidateTempAvatarPath ¶ added in v0.7.0
ValidateTempAvatarPath validates that a path points to an image file inside a kojo-avatar-* temp directory. Returns the resolved absolute path or an error. Used by handlers that accept user-supplied avatar paths.
func WriteCheckinFile ¶ added in v0.20.0
WriteCheckinFile is the thin wrapper around WriteWorkspaceFile for kind="checkin". Empty / whitespace-only content tombstones the DB row and removes the disk mirror.
Takes the store explicitly so callers (Manager.Load migration, REST handler) don't have to reach into a package-level global. ctx applies to the DB upsert; the disk mirror write is best-effort and not cancelled by ctx.
func WriteUserFile ¶ added in v0.20.0
WriteUserFile is the thin wrapper around WriteWorkspaceFile for kind="user". Empty / whitespace-only content tombstones the DB row and removes the disk mirror; non-empty content upserts and mirrors.
func WriteWorkspaceFile ¶ added in v0.101.0
func WriteWorkspaceFile(ctx context.Context, st *store.Store, agentID string, kind store.WorkspaceFileKind, body, ifMatchETag string) (*store.AgentWorkspaceFileRecord, error)
WriteWorkspaceFile is the DB-first writer for the agent_workspace_files table. Empty / whitespace-only body tombstones the row and removes the disk mirror; non-empty body upserts the row and mirrors to disk.
The DB is canonical; disk is a local mirror. A disk-mirror write failure triggers an inline reconcile (overwrite disk from the DB row we just wrote) so disk converges before we return; if the reconcile also fails we log and still report success — the DB is canonical and the next ReconcileWorkspaceFilesDiskFromDBHeld sweep will catch up.
Locking: holds the per-agent memorySyncMu across the DB write AND the disk mirror so a concurrent SyncWorkspaceFilesFromDisk (driven by prepareChat on every hot path) can't read the stale disk between our commit and our mirror and UPSERT yesterday's body back into the DB, silently losing the write. Same lock the memory_sync paths take.
ifMatchETag is forwarded to the store as the optimistic-concurrency precondition. Empty means "unconditional"; non-empty + stale surfaces store.ErrETagMismatch.
Types ¶
type Agent ¶
type Agent struct {
ID string `json:"id"`
Name string `json:"name"`
Persona string `json:"persona"` // persona description (markdown)
Model string `json:"model"` // e.g. "sonnet", "opus"
Effort string `json:"effort,omitempty"` // claude/grok/codex effort level
Tool string `json:"tool"` // CLI tool: "claude", "codex", "grok"
WorkDir string `json:"workDir,omitempty"` // file storage directory (empty = agentDir)
// CronExpr is a 5-field standard cron expression (M H DOM Mon DOW).
// Empty = scheduling disabled. Validated via ValidateCronExpr; rejected
// expressions never reach this field.
CronExpr string `json:"cronExpr"`
TimeoutMinutes int `json:"timeoutMinutes"` // max duration per cron run in minutes (0 = default 10)
// ResumeIdleMinutes is the idle-window threshold (in minutes) below
// which kojo keeps an over-token-threshold claude session via --resume
// instead of resetting. 0 = use defaultResumeIdleDuration (5 min).
// claude-only; ignored by other backends.
ResumeIdleMinutes int `json:"resumeIdleMinutes,omitempty"`
SilentStart string `json:"silentStart,omitempty"` // HH:MM — start of silent window (empty = no restriction)
SilentEnd string `json:"silentEnd,omitempty"` // HH:MM — end of silent window (empty = no restriction)
// NotifyDuringSilent controls whether the agent receives DM notifications
// during silent hours. Existing agents default to true (backward compat);
// new agents default to false.
NotifyDuringSilent *bool `json:"notifyDuringSilent,omitempty"`
// CronMessage is DEPRECATED. The canonical source for the
// periodic check-in body is the agent_workspace_files row with
// kind="checkin" (mirrored to <agentDir>/checkin.md). Manager.Load
// migrates any value still present in legacy settings_json into
// the workspace row and clears this field, so post-migration
// reads always observe an empty string. The struct field is
// retained strictly so settings_json round-trips through
// agentToSettings without erroring on the unknown key for rows
// written by older binaries.
CronMessage string `json:"cronMessage,omitempty"`
CreatedAt string `json:"createdAt"` // RFC3339
UpdatedAt string `json:"updatedAt"` // RFC3339
// LegacyIntervalMinutes is a transient field consumed by store.Load to
// migrate the old `intervalMinutes` JSON key into CronExpr. Cleared
// after normalization. Not written back: omitempty + zero value drops
// it from any subsequent serialization.
LegacyIntervalMinutes int `json:"intervalMinutes,omitempty"`
// Legacy fields — consumed by store.Load for activeStart/activeEnd →
// silentStart/silentEnd migration. Active hours are inverted to silent
// hours: silentStart = old activeEnd, silentEnd = old activeStart.
LegacyActiveStart string `json:"activeStart,omitempty"`
LegacyActiveEnd string `json:"activeEnd,omitempty"`
// HasAvatar indicates whether a custom avatar blob is resolvable
// for this agent (kojo://global/agents/<id>/avatar.<ext>; see
// internal/agent/avatar.go). Populated by Manager.avatarMeta on
// every Load / Get / Update from the blob body's presence on
// disk — the value is true whenever the body file exists, even
// in the rare case where the blob_refs cache row is stale or
// missing (the slice-1 fs-only mode).
HasAvatar bool `json:"hasAvatar"`
// AvatarHash is a cache-bust value the Web UI appends to its
// avatar URL (the AgentAvatar component embeds it AS-IS as
// ?t=<hash>; no client-side URL-encoding). An opaque cache-bust
// token from the API consumer's perspective; re-uploaded images
// bust the browser cache without changing the agent id. Two
// cases:
//
// HasAvatar=true: bare-hex sha256 of the blob body (the
// strong "sha256:<hex>" ETag from blob_refs
// with the prefix trimmed — the bare-hex
// convention preserves the historical
// AvatarHash format and keeps the query
// parameter visually clean). Falls back
// to a ModTime-derived hex when the
// blob_refs row hasn't been backfilled
// (slice-1 path / unit tests).
// HasAvatar=false: agent.UpdatedAt (an RFC3339 timestamp
// that contains colons — the Web UI embeds
// those literally in ?t=<value>; colons
// are permitted in URI query values per
// RFC 3986 so no escaping is required) —
// applyAvatarMeta substitutes it so the
// URL still busts caches across persona /
// settings edits that re-render the SVG
// fallback.
AvatarHash string `json:"avatarHash,omitempty"`
// PublicProfile is a short outward-facing description generated from persona.
// Shared with other agents via the directory endpoint. Does not expose internal persona details.
PublicProfile string `json:"publicProfile,omitempty"`
PublicProfileOverride bool `json:"publicProfileOverride,omitempty"`
// CustomBaseURL is the base URL for a custom Anthropic Messages API endpoint
// (e.g., llama-server). Only used when Tool is "custom".
CustomBaseURL string `json:"customBaseURL,omitempty"`
// AllowedTools is a whitelist of tool names forwarded to a custom endpoint.
// If non-empty, only listed tools are forwarded.
// If empty, all tools are forwarded.
AllowedTools []string `json:"allowedTools,omitempty"`
// AllowProtectedPaths lists protected directory names (claude, git, husky)
// for which Edit/Write/MultiEdit prompts should be suppressed. Recent
// claude-code versions guard these dirs even under bypassPermissions —
// explicit permissions.allow rules are the only bypass.
AllowProtectedPaths []string `json:"allowProtectedPaths,omitempty"`
// ThinkingMode controls reasoning/thinking for llama.cpp backend.
// "on" = enable, "off" = disable, "" = server default.
ThinkingMode string `json:"thinkingMode,omitempty"`
// SlackBot holds the Slack Socket Mode bot configuration for this agent.
SlackBot *SlackBotConfig `json:"slackBot,omitempty"`
// TTS holds per-agent text-to-speech configuration. Nil = TTS disabled
// (no synthesize/playback). The header toggle in the chat view is a
// session-level UI preference and lives in localStorage; this struct
// only governs voice/style/model when a synthesize call actually runs.
TTS *TTSConfig `json:"tts,omitempty"`
// LastMessage is a preview of the most recent message (for list display).
LastMessage *MessagePreview `json:"lastMessage,omitempty"`
// LastMessageAt is the epoch-millis timestamp of the most recent
// message. Millisecond precision (unlike LastMessage.Timestamp's
// seconds-resolution RFC3339 string) so the dashboard can order the
// agent list by most-recent activity without same-second ties
// reshuffling on every reload. Zero when the agent has no messages;
// such agents sort last (by CreatedAt) in the UI.
LastMessageAt int64 `json:"lastMessageAt,omitempty"`
// Archived is true when the agent has been archived via DELETE
// /api/v1/agents/{id}?archive=true. Archived agents retain most on-disk
// data (agent dir, credentials, notify tokens, messages, memory) but
// have all runtime activity stopped: cron, notify polling, one-shot
// chats, Slack bot, and inbound chats are refused with ErrAgentArchived.
// Group DM memberships are an exception — they are removed on archive
// (2-person groups are dissolved) and NOT restored on Unarchive.
// Restored via POST /api/v1/agents/{id}/unarchive.
Archived bool `json:"archived,omitempty"`
// ArchivedAt is the RFC3339 timestamp when Archived was last set.
// Cleared on unarchive.
ArchivedAt string `json:"archivedAt,omitempty"`
// Privileged grants the agent the ability to delete/reset other agents
// (but NOT to fork or read their full record). Owner-only mutation —
// the API strips this field from PATCH bodies and exposes a dedicated
// POST /api/v1/agents/{id}/privilege handler instead.
Privileged bool `json:"privileged,omitempty"`
// DeviceSwitchEnabled gates whether the §3.7 device-switch skill
// (kojo-switch-device) gets installed into the agent's .claude/skills
// directory. Nil = use the default (true) — agents created before
// the field landed default to enabled. Operator may disable per-
// agent via PATCH; the skill file is removed on the next chat
// prepare. The toggle is checked alongside peer count: a single-
// node install (no other peers registered) suppresses the skill
// regardless of this flag because there's nothing to switch to.
DeviceSwitchEnabled *bool `json:"deviceSwitchEnabled,omitempty"`
// LastTransferSkips records the session files the most recent
// inbound §3.7 device-switch transfer skipped (oversized JSONL,
// unreadable codex ref, …). Stamped into settings_json by the
// target's applyPeerAgentSync — cleared on a clean transfer —
// so the owner UI can show a "skipped during transfer" notice.
LastTransferSkips []SkippedSessionFile `json:"lastTransferSkips,omitempty"`
// HolderPeer is set only for agents that live on a remote peer
// (i.e. the agent's agent_locks.holder_peer != local peer). The
// Manager.List path populates this for agents whose runtime was
// released via §3.7 device-switch; the field is empty for
// locally-managed agents. The UI uses it to show a "remote"
// indicator and route chat traffic through the WS proxy.
HolderPeer string `json:"holderPeer,omitempty"`
// HolderPeerName is the human-friendly name of HolderPeer,
// resolved from peer_registry at list time so the dashboard
// doesn't have to do its own lookup. Empty when HolderPeer is
// empty or the registry has no row for the holder (a race
// against decommission, or a freshly switched-to peer that
// hasn't broadcast its registration yet).
HolderPeerName string `json:"holderPeerName,omitempty"`
// DisabledInjections is the set of context-injection section keys
// the Owner has switched off for this agent. Empty / absent = all
// sections enabled (the historical behavior). Keys are validated
// against validInjectionKeys on PATCH; see InjectionDisabled for
// the read side. Persisted inside agents.settings_json like every
// other per-agent setting — no schema migration required, absent
// key decodes to nil.
//
// Owner-only on the API surface: handleUpdateAgent rejects a PATCH
// body carrying this field from a non-Owner principal, because
// toggling injections changes the agent's capability surface.
DisabledInjections []string `json:"disabledInjections,omitempty"`
// AutoEffort gates the per-turn dynamic effort classifier (see
// resolveTurnEffort). Nil = enabled — the feature is opt-OUT so
// existing agents pick it up without a migration. When enabled,
// the configured Effort acts as the ceiling/fallback rather than
// the fixed per-turn value. Persisted inside agents.settings_json
// like every other per-agent setting — no schema migration.
AutoEffort *bool `json:"autoEffort,omitempty"`
// HolderPeerStatus mirrors peer_registry.status for HolderPeer at
// list/read time so the UI can grey out chat (and surface an
// "offline" banner) without a second round-trip to /api/v1/peers.
// One of "online" / "offline" / "degraded". Empty when HolderPeer
// is empty or the registry has no row for the holder.
HolderPeerStatus string `json:"holderPeerStatus,omitempty"`
// Busy is a runtime-only flag set by Manager.List when the agent has
// an in-flight interactive/cron chat (see Manager.IsBusy). Never
// persisted — stripped from settings_json on Save and Load like the
// holder* fields. The dashboard folds it into its "N running" figure
// so a chatting agent registers even when no terminal session is up.
Busy bool `json:"busy,omitempty"`
// AwaitingAnswer is a runtime-only flag set by Manager.List when the
// agent's running turn has raised an AskUserQuestion prompt that is
// still unanswered (see Manager.HasPendingQuestion). Never persisted —
// stripped from settings_json on Save and Load like Busy and the
// holder* fields. The dashboard highlights the agent row so a
// blocked-on-human turn is obvious at a glance.
AwaitingAnswer bool `json:"awaitingAnswer,omitempty"`
}
Agent represents a persistent AI persona (friend).
func (*Agent) InjectionDisabled ¶ added in v0.111.0
InjectionDisabled reports whether the given context-injection section is switched off for this agent. Nil agent / empty set = enabled.
func (*Agent) IsAutoEffortEnabled ¶ added in v0.111.0
IsAutoEffortEnabled reports whether the per-turn dynamic effort classifier is enabled for this agent. Nil pointer = default true (opt-out feature) so agents predating the field participate.
func (*Agent) IsDeviceSwitchEnabled ¶ added in v0.101.0
IsDeviceSwitchEnabled reports whether the §3.7 device-switch skill should be installed for this agent. Nil pointer = default true so agents predating the field, and operators who never touched the toggle, get the skill auto-installed. A peer count of zero (single- node install) still suppresses the file — see SyncDeviceSwitchSkill.
func (*Agent) ResumeIdleDuration ¶ added in v0.15.0
ResumeIdleDuration returns the configured idle window for keeping an over-token-threshold claude session via --resume. ResumeIdleMinutes==0 (the default for legacy agents) maps to defaultResumeIdleDuration so existing behavior is preserved. Values outside the validated whitelist (e.g. left over from a hand-edited agents.json or a future schema) also fall back to the default — the API layer's whitelist must not be the only line of defence.
func (*Agent) ShouldNotifyDuringSilent ¶ added in v0.18.0
ShouldNotifyDuringSilent returns whether the agent should receive DM notifications while in silent hours. Existing agents with a nil pointer default to true for backward compatibility; new agents default to false.
type AgentConfig ¶
type AgentConfig struct {
Name string `json:"name"`
Persona string `json:"persona"`
Model string `json:"model"`
Effort string `json:"effort"`
Tool string `json:"tool"`
CustomBaseURL string `json:"customBaseURL"`
ThinkingMode string `json:"thinkingMode"`
WorkDir string `json:"workDir"`
// CronExpr is the 5-field cron expression for periodic check-ins.
// nil = use default ("*/30 * * * *" with per-agent offset).
// "" = scheduling explicitly disabled.
// other = validated via ValidateCronExpr.
CronExpr *string `json:"cronExpr"`
// LegacyIntervalMinutes catches old clients still posting the pre-CronExpr
// `intervalMinutes` field so we can reject the request with a clear error
// instead of silently dropping the value (Go's default Unmarshal behaviour
// for unknown fields). Validated in newAgent / Manager.Update.
LegacyIntervalMinutes *int `json:"intervalMinutes,omitempty"`
TimeoutMinutes *int `json:"timeoutMinutes"` // nil = use default (0 = 10 min)
// ResumeIdleMinutes overrides the per-agent claude --resume idle window.
// nil/0 = use defaultResumeIdleDuration (5 min).
ResumeIdleMinutes *int `json:"resumeIdleMinutes"`
SilentStart *string `json:"silentStart"` // HH:MM or empty
SilentEnd *string `json:"silentEnd"` // HH:MM or empty
NotifyDuringSilent *bool `json:"notifyDuringSilent"` // nil = use default (false for new)
CronMessage *string `json:"cronMessage"` // nil/empty = use default trailing instruction
// DeviceSwitchEnabled gates the kojo-switch-device skill. nil =
// default (true). Persisted into Agent.DeviceSwitchEnabled with
// no normalization so the "default" semantic survives schema
// evolution.
DeviceSwitchEnabled *bool `json:"deviceSwitchEnabled"`
}
AgentConfig is the request body for creating an agent.
type AgentTokenStore ¶ added in v0.17.0
type AgentTokenStore interface {
EnsureAgentToken(agentID string) error
RemoveAgentToken(agentID string)
}
AgentTokenStore is the minimal contract the agent manager needs from the auth token store, narrowed so the agent package does not import internal/auth (avoids a layering cycle).
type AgentUpdateConfig ¶
type AgentUpdateConfig struct {
Name *string `json:"name"`
Persona *string `json:"persona"`
PublicProfile *string `json:"publicProfile"`
PublicProfileOverride *bool `json:"publicProfileOverride"`
Model *string `json:"model"`
Effort *string `json:"effort"`
Tool *string `json:"tool"`
WorkDir *string `json:"workDir"`
CronExpr *string `json:"cronExpr"`
// LegacyIntervalMinutes mirrors the AgentConfig field — catches old clients
// still PATCH-ing intervalMinutes so we can reject with a clear error.
LegacyIntervalMinutes *int `json:"intervalMinutes,omitempty"`
TimeoutMinutes *int `json:"timeoutMinutes"`
ResumeIdleMinutes *int `json:"resumeIdleMinutes"`
SilentStart *string `json:"silentStart"`
SilentEnd *string `json:"silentEnd"`
NotifyDuringSilent *bool `json:"notifyDuringSilent"`
// CronMessage follows the standard *string PATCH convention used by every
// other field on this struct: nil/omitted = leave unchanged, "" = clear
// back to the built-in default trailing instruction.
CronMessage *string `json:"cronMessage"`
CustomBaseURL *string `json:"customBaseURL"`
ThinkingMode *string `json:"thinkingMode"`
AllowedTools []string `json:"allowedTools"`
AllowProtectedPaths *[]string `json:"allowProtectedPaths"`
// TTS replaces the entire TTSConfig when non-nil. Pass an explicit
// {enabled:false} to keep the field but disable synthesis; pass a
// fully-populated struct to enable.
TTS *TTSConfig `json:"tts,omitempty"`
// DeviceSwitchEnabled toggles the kojo-switch-device skill. nil
// in the body = "not provided; leave as-is"; explicit true/false
// overwrites the agent's flag. The next prepareChat / skill sync
// installs or removes the SKILL.md accordingly.
DeviceSwitchEnabled *bool `json:"deviceSwitchEnabled"`
// DisabledInjections replaces the whole set when non-nil; pass an
// empty array to re-enable everything. Owner-only (enforced at the
// HTTP handler). Keys validated via ValidateDisabledInjections.
DisabledInjections *[]string `json:"disabledInjections"`
// AutoEffort toggles the per-turn dynamic effort classifier. nil =
// "not provided; leave as-is"; explicit true/false overwrites.
// Plain self-PATCHable field (like deviceSwitchEnabled) — not
// owner-only.
AutoEffort *bool `json:"autoEffort"`
}
AgentUpdateConfig is the request body for PATCH updates. Pointer fields distinguish "not provided" (nil) from "set to zero".
func (*AgentUpdateConfig) HubLocalOnly ¶ added in v0.111.0
func (cfg *AgentUpdateConfig) HubLocalOnly() bool
HubLocalOnly reports whether the update touches ONLY hub-local-safe fields (an empty config counts — applying it is a harmless no-op). Struct-based twin of IsHubLocalSafePatchKey used on paths that already decoded the body.
type AnswerFunc ¶ added in v0.113.0
AnswerFunc resolves a pending interactive AskUserQuestion by writing the CLI control_response for requestID. When deny is true the tool call is refused with denyMessage; otherwise answers maps each question string to the chosen answer (a label, a ", "-joined list of labels for multiSelect, or a free-form string). Returns ErrQuestionNotFound if requestID is not pending, or ErrAgentNotBusy if the turn already ended.
type ArrivalNotes ¶ added in v0.111.0
type ArrivalNotes struct {
TokenReissued bool
DegradedFlushes []string
TransferSkips []SkippedSessionFile
}
ArrivalNotes carries per-switch caveats into the arrival prompt so the just-arrived agent knows what state did NOT make the trip:
- TokenReissued: target lacked the raw $KOJO_AGENT_TOKEN and auto-re-issued it at finalize (no manual re-issue needed).
- DegradedFlushes: the switch ran in degraded mode; the listed source-side flushes ("memory_flush", "persona_flush") were skipped, so memory / persona may be stale.
- TransferSkips: session files dropped from the sync payload.
type AttachmentForwarder ¶ added in v0.101.0
type AttachmentForwarder func(ctx context.Context, scope blob.Scope, path string, sha256Hex string, body io.Reader, size int64) error
AttachmentForwarder is the callback Manager invokes when the daemon is running as a non-hub peer and the bytes for an agent-attached file must also be pushed to the hub blob store (the UI lives on hub, so without this push the assistant's reply renders an unreachable kojo:// URI).
The forwarder is given the canonical (scope, path) pair plus the body, the digest (pre-computed during the local Put), and the size. Implementations should use the peer Identity to sign the outbound request and target hub's blob ingest endpoint.
A non-nil error is logged but not fatal — the local copy lands regardless. Caller continues with the next file. Returning nil when the forward succeeded is the normal path.
type BackgroundTurnFunc ¶ added in v0.112.0
type BackgroundTurnFunc func(agentID string, events <-chan ChatEvent, answer AnswerFunc, abort func())
BackgroundTurnFunc consumes an unsolicited turn's events (a background subagent notification arriving on the persistent process with no user input) and persists + broadcasts them. The channel is closed by the session when the turn's result arrives. answer resolves an AskUserQuestion raised during the turn (nil if the session has no answer channel), letting a watching human respond to a question on an otherwise-automated turn. abort interrupts the turn on the CLI itself (nil when the turn is already complete, e.g. an absorbed racing notification). Without it the Manager's Abort could only cancel event processing — the CLI kept generating, so a stop pressed on an unsolicited turn (including the auto-turn a queued steer starts after an abort) looked accepted and then visibly resumed, and the busy slot stayed wedged until a kojo restart.
type BusySource ¶ added in v0.18.1
type BusySource int
BusySource identifies what triggered a busy state.
const ( // BusySourceUser is an interactive chat initiated by the user. BusySourceUser BusySource = iota // BusySourceCron is a scheduled cron check-in. BusySourceCron // BusySourceNotification is an automated notification (group DM, etc.) // that should not surface as "busy" in member status displays. BusySourceNotification )
func (BusySource) String ¶ added in v0.114.0
func (s BusySource) String() string
String renders the busy source as a short lowercase tag, used in DrainBlockers output and logs.
type ChatBackend ¶
type ChatBackend interface {
// Chat sends a message and returns a channel of streaming events.
// The channel is closed when the response is complete.
Chat(ctx context.Context, agent *Agent, userMessage string, systemPrompt string, opts ChatOptions) (<-chan ChatEvent, error)
// Name returns the tool identifier (e.g. "claude", "codex", "grok").
Name() string
// Available returns true if the CLI tool is installed and accessible.
Available() bool
}
ChatBackend abstracts a CLI tool for agent chat.
type ChatEvent ¶
type ChatEvent struct {
Type string `json:"type"` // "status", "text", "thinking", "tool_use", "tool_result", "done", "error", "message", "attachment"
Status string `json:"status,omitempty"`
Delta string `json:"delta,omitempty"`
ToolUseID string `json:"toolUseId,omitempty"`
ToolName string `json:"toolName,omitempty"`
ToolInput string `json:"toolInput,omitempty"`
ToolOutput string `json:"toolOutput,omitempty"`
Message *Message `json:"message,omitempty"`
Attachments []MessageAttachment `json:"attachments,omitempty"` // streamed kojo-attach files
Usage *Usage `json:"usage,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
// ParentToolUseID is set when this event originates from a subagent
// spawned by a Task tool call rather than the main assistant turn.
// Its value is the tool_use ID of the parent Task invocation (or, for
// a nested sub-subagent, the ID of an intermediate subagent tool call
// — callers should resolve that up to the nearest top-level Task via
// the same flat-owner logic parseClaudeStream uses). Consumers that
// accumulate the main turn's text/toolUses MUST skip events carrying
// a non-empty ParentToolUseID; the UI instead nests them under the
// matching Task tool chip.
ParentToolUseID string `json:"parentToolUseId,omitempty"`
// RequestID identifies a control_request the CLI raised for an
// interactive tool (currently AskUserQuestion). Set only on
// "user_question" events; the Web UI echoes it back to
// POST /agents/{id}/answer so the server can pair the answer with the
// blocked control_request.
RequestID string `json:"requestId,omitempty"`
// Questions carries the raw AskUserQuestion input.questions array
// (the CLI's tool input) so the UI can render the question card. Only
// populated on "user_question" events.
Questions json.RawMessage `json:"questions,omitempty"`
// RateLimit carries the latest rate-limit snapshot parsed from the
// backend stream. Populated only on "rate_limit" events, which arrive
// mid-turn whenever the Claude CLI reports a usage threshold crossing.
RateLimit *RateLimitInfo `json:"rateLimit,omitempty"`
}
ChatEvent is streamed from backend to WebSocket during a chat.
type ChatOptions ¶ added in v0.10.0
type ChatOptions struct {
// OneShot skips session resumption, running a fresh ephemeral session.
// Used for Slack and other external platform conversations that have
// their own conversation context.
OneShot bool
// MCPServers is the set of MCP tool servers to make available for this
// chat session. Each backend injects them in its own way (CLI args,
// config files, etc.). May be nil if no MCP servers are configured.
MCPServers map[string]mcpServerEntry
// AutomatedTrigger marks the chat as a non-interactive system fire
// (cron, groupdm notification, Slack one-shot, etc.) rather than a
// human-driven turn. Backends use this to disable the idle-window
// guard on session resets: there is no interactive conversation to
// preserve, so token conservation wins over continuity.
AutomatedTrigger bool
// SessionKey overrides the default agent-ID-based session identifier.
// The key is hashed into a deterministic UUID so callers can pass any
// stable string (e.g. "slack:<channel>:<thread>") to get an independent
// Claude session JSONL. Used for per-Slack-thread session resumption
// where each conversation thread maintains its own context, isolated
// from the agent's WebUI session and from other threads.
//
// Empty string means "fall back to the agent-wide session ID" — i.e.
// the existing single-session-per-agent behaviour. Backends that don't
// honor SessionKey (see backendSupportsSessionKey) must treat any
// non-empty value as if it were empty and degrade to OneShot=true at
// the call site rather than silently leaking context across keys.
SessionKey string
// RecentMessagesContext is a short, bounded transcript excerpt the
// backend may prepend when it has to start a fresh persistent session
// instead of resuming an existing one. Claude uses this as a continuity
// fallback for missing/empty/reset JSONL sessions; when --resume works,
// it is intentionally ignored to avoid duplicating history.
RecentMessagesContext string
// SystemPromptExtra is appended verbatim to the systemPrompt argument
// AFTER the backend's normal prompt assembly. Use it to inject
// per-conversation context (e.g. Slack channel/thread info) without
// disturbing the cached system-prompt prefix shared across turns
// within the same session.
//
// Why a separate field instead of pre-concatenating into systemPrompt:
// the volatile per-turn data must land at a stable offset so the
// claude prompt cache treats the leading shared prompt as a cacheable
// prefix. Mixing channel-context tokens into the middle of the prompt
// would invalidate the cache for every Slack message.
SystemPromptExtra string
// OnSteerReady, if set, is invoked once by a backend that supports
// mid-turn steering (claude: extra user-message line on stdin; codex:
// turn/steer JSON-RPC) as soon as the turn can accept input. The callback
// receives a SteerFunc the caller can register (e.g. in a busy-turn
// handle) and call later to inject text into the running turn.
// Backends that don't support steering simply never call this.
OnSteerReady func(SteerFunc)
// OnQuestionReady, if set, is invoked once by a backend that supports
// interactive AskUserQuestion prompts (claude: --permission-prompt-tool
// stdio) as soon as the turn can accept answers. The callback receives an
// AnswerFunc the caller can register (e.g. on a busy-turn handle) and call
// later to unblock a pending question. Backends that can't surface the
// prompt to a user (or automated turns) simply never call this and
// auto-deny/auto-allow control_requests internally.
OnQuestionReady func(AnswerFunc)
// OnQuestionResolved, if set, is invoked by a backend every time a
// pending AskUserQuestion prompt is resolved — whether by an explicit
// answer/deny (via the AnswerFunc handed to OnQuestionReady), an
// automated-turn timeout, or a deny issued because no UI ever picked
// the question up. It receives the same requestID the question was
// raised under (see ChatEvent.RequestID on the "user_question" event),
// so the caller can clear whatever pending-question bookkeeping it
// keyed on that ID. May fire more than once for the same requestID if
// two resolution paths race (harmless — callers must make clearing
// idempotent). Backends that never surface questions simply never
// call this.
OnQuestionResolved func(requestID string)
}
ChatOptions holds optional parameters for a chat invocation.
type ClaudeBackend ¶
type ClaudeBackend struct {
// contains filtered or unexported fields
}
ClaudeBackend implements ChatBackend using the Claude CLI with stream-json output.
func NewClaudeBackend ¶
func NewClaudeBackend(logger *slog.Logger) *ClaudeBackend
func (*ClaudeBackend) Available ¶
func (b *ClaudeBackend) Available() bool
func (*ClaudeBackend) Chat ¶
func (b *ClaudeBackend) Chat(ctx context.Context, agent *Agent, userMessage string, systemPrompt string, opts ChatOptions) (<-chan ChatEvent, error)
func (*ClaudeBackend) CloseAllSessions ¶ added in v0.112.0
func (b *ClaudeBackend) CloseAllSessions()
CloseAllSessions terminates every live persistent process. Used by the restart drain so a device switch / restart doesn't strand a process.
func (*ClaudeBackend) CloseSession ¶ added in v0.112.0
func (b *ClaudeBackend) CloseSession(agentID string)
CloseSession terminates any persistent process for the agent. Used by the Manager on reset, restart drain, device-switch quiesce, and archive/delete.
func (*ClaudeBackend) CloseSessionSync ¶ added in v0.112.0
func (b *ClaudeBackend) CloseSessionSync(agentID string)
CloseSessionSync terminates the agent's persistent process AND waits (bounded) for it to fully exit before returning. Destructive lifecycle paths (context-threshold reset, archive/delete, device-switch handoff) delete or transfer the session JSONL immediately afterward and must not race the old process re-creating files during its async close grace. The restart drain, which only needs the process gone eventually, keeps the async CloseSession.
func (*ClaudeBackend) HasLiveSession ¶ added in v0.112.0
func (b *ClaudeBackend) HasLiveSession(agentID string) bool
HasLiveSession reports whether a persistent process currently exists for the agent (used by lifecycle idle checks).
func (*ClaudeBackend) Name ¶
func (b *ClaudeBackend) Name() string
func (*ClaudeBackend) SessionInTurn ¶ added in v0.112.0
func (b *ClaudeBackend) SessionInTurn(agentID string) bool
SessionInTurn reports whether the agent's persistent session is actively processing a turn (solicited or unsolicited). An idle live session is NOT busy for quiesce purposes.
func (*ClaudeBackend) SetBackgroundTurnHandler ¶ added in v0.112.0
func (b *ClaudeBackend) SetBackgroundTurnHandler(fn BackgroundTurnFunc)
SetBackgroundTurnHandler registers the Manager callback used to surface unsolicited (background subagent notification) turns.
func (*ClaudeBackend) SetProxyURL ¶ added in v0.10.0
func (b *ClaudeBackend) SetProxyURL(url string)
SetProxyURL configures an ANTHROPIC_BASE_URL to inject into Claude CLI env.
func (*ClaudeBackend) SetRateLimitHandler ¶ added in v0.114.0
func (b *ClaudeBackend) SetRateLimitHandler(fn func(agentID string, info RateLimitInfo))
SetRateLimitHandler registers the Manager callback used to record rate-limit telemetry that arrives while the persistent session is idle.
func (*ClaudeBackend) SetSubagentActivityHandler ¶ added in v0.114.0
func (b *ClaudeBackend) SetSubagentActivityHandler(fn subagentActivityFunc)
SetSubagentActivityHandler registers the Manager callback used to surface live/backfilled background-subagent output (Option A + C).
type ClaudeSessionFile ¶ added in v0.101.0
ClaudeSessionFile is one transferable JSONL entry: the session UUID (filename without .jsonl) plus the raw file body. Caller base64-encodes for the wire if the transport demands plain JSON.
type CodexBackend ¶
type CodexBackend struct {
// contains filtered or unexported fields
}
CodexBackend implements ChatBackend for the Codex CLI using app-server (JSON-RPC 2.0 over stdio) for real streaming support.
func NewCodexBackend ¶
func NewCodexBackend(logger *slog.Logger) *CodexBackend
func (*CodexBackend) Available ¶
func (b *CodexBackend) Available() bool
func (*CodexBackend) Chat ¶
func (b *CodexBackend) Chat(ctx context.Context, agent *Agent, userMessage string, systemPrompt string, opts ChatOptions) (<-chan ChatEvent, error)
func (*CodexBackend) Name ¶
func (b *CodexBackend) Name() string
type CodexSQLiteRow ¶ added in v0.101.5
type CodexSQLiteRow struct {
Columns []string `json:"columns"`
Values []CodexSQLiteValue `json:"values"`
}
type CodexSQLiteValue ¶ added in v0.101.5
type CodexSessionTransfer ¶ added in v0.101.5
type CodexSessionTransfer struct {
Threads []CodexThreadTransfer `json:"threads"`
}
type CodexThreadTransfer ¶ added in v0.101.5
type CodexThreadTransfer struct {
RefName string `json:"ref_name"`
ThreadID string `json:"thread_id"`
RolloutRelPath string `json:"rollout_rel_path"`
RolloutContent []byte `json:"rollout_content"`
ThreadRow *CodexSQLiteRow `json:"thread_row,omitempty"`
DynamicToolRows []CodexSQLiteRow `json:"dynamic_tool_rows,omitempty"`
}
type Credential ¶
type Credential struct {
ID string `json:"id"`
Label string `json:"label"`
Username string `json:"username"`
Password string `json:"password"`
TOTPSecret string `json:"totpSecret,omitempty"`
TOTPAlgorithm string `json:"totpAlgorithm,omitempty"` // SHA1 (default), SHA256, SHA512
TOTPDigits int `json:"totpDigits,omitempty"` // 6 (default) or 8
TOTPPeriod int `json:"totpPeriod,omitempty"` // 30 (default)
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
Credential represents a stored ID/password pair for an agent.
type CredentialStore ¶
type CredentialStore struct {
// contains filtered or unexported fields
}
CredentialStore manages encrypted credential storage in SQLite.
func NewCredentialStore ¶
func NewCredentialStore() (*CredentialStore, error)
NewCredentialStore opens or creates the encrypted credential store.
func (*CredentialStore) AddCredential ¶
func (s *CredentialStore) AddCredential(agentID, label, username, password string, totp *TOTPParams) (*Credential, error)
AddCredential adds a new credential for an agent.
func (*CredentialStore) Close ¶
func (s *CredentialStore) Close() error
Close closes the database connection.
func (*CredentialStore) DeleteAllForAgent ¶
func (s *CredentialStore) DeleteAllForAgent(agentID string) error
DeleteAllForAgent removes all credentials for an agent.
func (*CredentialStore) DeleteCredential ¶
func (s *CredentialStore) DeleteCredential(agentID, credID string) error
DeleteCredential removes a credential by ID.
func (*CredentialStore) DeleteToken ¶ added in v0.7.0
func (s *CredentialStore) DeleteToken(provider, agentID, sourceID, key string) error
DeleteToken removes a single token.
func (*CredentialStore) DeleteTokensByAgent ¶ added in v0.7.0
func (s *CredentialStore) DeleteTokensByAgent(agentID string) error
DeleteTokensByAgent removes all notify tokens for an agent.
func (*CredentialStore) DeleteTokensBySource ¶ added in v0.7.0
func (s *CredentialStore) DeleteTokensBySource(provider, agentID, sourceID string) error
DeleteTokensBySource removes all tokens for a specific source.
func (*CredentialStore) ExportCredentials ¶ added in v0.101.0
func (s *CredentialStore) ExportCredentials(agentID string) ([]*Credential, error)
ExportCredentials returns every stored credential for an agent with password and TOTP secret DECRYPTED. Intended for the §3.7 device- switch peer-sync path: credentials.db is per-peer (encrypted with a peer-local key), so cross-peer transport has to be in plaintext. The wire side is bounded by tsnet peer auth + HTTPS; the receiver re-encrypts with its own credentials.key before persisting.
Returns an empty slice (NOT nil) when the agent has no credentials, so callers can range over the result and a JSON-omitempty marshal drops the field for the common no-credential case.
func (*CredentialStore) GetSetting ¶ added in v0.12.0
func (s *CredentialStore) GetSetting(key string) string
GetSetting retrieves a global setting value. Returns "" if the key is not found. Unexpected database errors (connection issues, schema drift, etc.) are logged rather than silently swallowed so they don't masquerade as a missing value.
func (*CredentialStore) GetTOTPCode ¶
func (s *CredentialStore) GetTOTPCode(agentID, credID string) (string, int64, error)
GetTOTPCode generates the current TOTP code for a credential.
func (*CredentialStore) GetToken ¶ added in v0.7.0
func (s *CredentialStore) GetToken(provider, agentID, sourceID, key string) (string, error)
GetToken retrieves and decrypts a token value.
func (*CredentialStore) ListCredentials ¶
func (s *CredentialStore) ListCredentials(agentID string) ([]*Credential, error)
ListCredentials returns all credentials for an agent with secrets masked. Passwords and TOTP secrets are NOT decrypted — only metadata is returned.
func (*CredentialStore) ReplaceCredentials ¶ added in v0.101.0
func (s *CredentialStore) ReplaceCredentials(agentID string, creds []*Credential) error
ReplaceCredentials atomically replaces every credential for agentID with the supplied set. Source-wins, full-replace — matches the §3.7 device-switch semantics of agent_messages / memory_entries / etc: the source peer is authoritative for the agent's runtime state, so the target's prior view (if any) is discarded wholesale.
Each supplied record's plaintext Password / TOTPSecret is re-encrypted with the target's credentials.key (since the source's key is not transported). IDs and timestamps are preserved verbatim so the UI's stable ordering and "createdAt" surface stays the same across peers.
A nil/empty creds slice CLEARS the agent's credentials on target — mirrors AgentSyncPayload's "empty = clear" promise for the rest of the surfaces. Idempotent under retry: the DELETE inside the tx re-runs cleanly; the INSERT writes the same rows.
func (*CredentialStore) RevealPassword ¶
func (s *CredentialStore) RevealPassword(agentID, credID string) (string, error)
RevealPassword returns the plaintext password for a credential.
func (*CredentialStore) SetSetting ¶ added in v0.12.0
func (s *CredentialStore) SetSetting(key, value string) error
SetSetting stores a global setting value.
func (*CredentialStore) SetToken ¶ added in v0.7.0
func (s *CredentialStore) SetToken(provider, agentID, sourceID, key, value string, expiresAt time.Time) error
SetToken stores an encrypted token value. Use empty agent_id/source_id for global tokens (currently only the Slack bot's per-agent app/bot tokens use this, with sourceID="").
func (*CredentialStore) UpdateCredential ¶
func (s *CredentialStore) UpdateCredential(agentID, credID string, label, username, password *string, totp *TOTPParams) (*Credential, error)
UpdateCredential updates an existing credential. Only non-nil fields are applied.
type CustomBackend ¶ added in v0.10.1
type CustomBackend struct {
// contains filtered or unexported fields
}
CustomBackend implements ChatBackend for custom Anthropic Messages API endpoints (e.g., llama-server). It delegates to ClaudeBackend with ANTHROPIC_BASE_URL pointed at the custom endpoint.
func NewCustomBackend ¶ added in v0.10.1
func NewCustomBackend(logger *slog.Logger) *CustomBackend
func (*CustomBackend) Available ¶ added in v0.10.1
func (b *CustomBackend) Available() bool
Available returns true if the claude CLI is in PATH (required as the client).
func (*CustomBackend) Chat ¶ added in v0.10.1
func (b *CustomBackend) Chat(ctx context.Context, agent *Agent, userMessage string, systemPrompt string, opts ChatOptions) (<-chan ChatEvent, error)
func (*CustomBackend) Name ¶ added in v0.10.1
func (b *CustomBackend) Name() string
type DirectoryEntry ¶
type DirectoryEntry struct {
ID string `json:"id"`
Name string `json:"name"`
PublicProfile string `json:"publicProfile"`
}
DirectoryEntry is the minimal public info shared with other agents.
type ForkOptions ¶ added in v0.11.0
ForkOptions controls what state is copied into the forked agent.
type GrokBackend ¶ added in v0.101.0
type GrokBackend struct {
// contains filtered or unexported fields
}
GrokBackend implements ChatBackend for the Grok CLI (https://github.com/xai-org/grok-cli). It drives `grok` headlessly via --prompt-file plus --output-format streaming-json and parses the resulting thought/text/end events into ChatEvents.
Session continuity uses an explicit per-agent session ID captured from the streaming "end" event, persisted to <agentDir>/.grok/session_id, and replayed on the next non-OneShot turn via `--resume <id>`. We do NOT rely on grok's `--continue` (which picks the most-recently- modified session for a cwd) because that would let an OneShot Slack thread silently take over the agent's primary session — see the Codex review on the original `--continue` implementation.
OneShot turns never read or write the stored session ID and the session directory grok creates for them is removed after the turn completes, so the persistent session stays isolated.
MCP injection is intentionally unsupported here: the grok CLI loads MCP servers from `~/.grok/config.toml` (global) or a project-scoped `<cwd>/.grok/config.toml`. Per-session inline overrides are not available. When opts.MCPServers is non-empty we log a warning so the caller can see that the configured servers were dropped — use the operator's global grok config to wire MCP for now.
func NewGrokBackend ¶ added in v0.101.0
func NewGrokBackend(logger *slog.Logger) *GrokBackend
func (*GrokBackend) Available ¶ added in v0.101.0
func (b *GrokBackend) Available() bool
func (*GrokBackend) Chat ¶ added in v0.101.0
func (b *GrokBackend) Chat(ctx context.Context, agent *Agent, userMessage string, systemPrompt string, opts ChatOptions) (<-chan ChatEvent, error)
func (*GrokBackend) Name ¶ added in v0.101.0
func (b *GrokBackend) Name() string
type GrokSessionFile ¶ added in v0.101.0
GrokSessionFile is one transferable file from a grok session directory. RelPath is the path RELATIVE to the session UUID directory (e.g. "events.jsonl" or "terminal/call-abc-1.log"). Caller base64-encodes Content for the wire if needed.
type GrokSessionTransfer ¶ added in v0.101.0
type GrokSessionTransfer struct {
SessionID string
Files []GrokSessionFile
}
GrokSessionTransfer is the complete handoff payload for a grok agent. SessionID is the UUID that goes into `.grok/session_id` on target; Files are the contents of source's `$GROK_HOME/sessions/<encoded(absAgentDir)>/<sessionID>/` subtree. The wire layer is responsible for base64 of the file bodies if the transport is text-only.
type GroupDM ¶
type GroupDM struct {
ID string `json:"id"`
Name string `json:"name"`
Members []GroupMember `json:"members"`
Cooldown int `json:"cooldown"` // notification cooldown in seconds (0 = use default)
Style GroupDMStyle `json:"style"` // communication style: "efficient" or "expressive"
// Venue is the physical setting hint. "chatroom" (default) for a closed
// online chat, "colocated" when members are co-present in real space.
Venue GroupDMVenue `json:"venue,omitempty"`
// MaxHops caps the agent-to-agent notification relay depth for this
// room. 0 means "use defaultMaxHops". See PostMessage hop semantics.
MaxHops int `json:"maxHops,omitempty"`
// Kind is "group" (default) or "dm" (first-class 1:1 room sugar).
Kind GroupDMKind `json:"kind,omitempty"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type GroupDMKind ¶ added in v0.111.0
type GroupDMKind string
GroupDMKind partitions rooms into classic groups and 1:1 DMs.
const ( GroupDMKindGroup GroupDMKind = "group" GroupDMKindDM GroupDMKind = "dm" // GroupDMKindThread is a parallel human↔agent thread room. Unlike a // single-agent "dm" it is always created fresh (no member-set dedup) so // an agent can have many independent threads at once. Behaves like a // single-agent dm for the thread-turn / archive / UI paths. GroupDMKindThread GroupDMKind = "thread" )
type GroupDMManager ¶
type GroupDMManager struct {
// contains filtered or unexported fields
}
GroupDMManager manages group DM CRUD, message posting, and notifications.
func NewGroupDMManager ¶
func NewGroupDMManager(agentMgr *Manager, logger *slog.Logger) *GroupDMManager
NewGroupDMManager creates a new GroupDMManager.
func (*GroupDMManager) APIBase ¶
func (m *GroupDMManager) APIBase() string
APIBase returns the current API base URL.
func (*GroupDMManager) AddMember ¶ added in v0.8.1
func (m *GroupDMManager) AddMember(id, newAgentID, callerAgentID string) (*GroupDM, error)
AddMember adds an agent to an existing group DM. callerAgentID must be a current member. Notifies existing members about the new addition.
func (*GroupDMManager) CheckMembership ¶ added in v0.9.0
func (m *GroupDMManager) CheckMembership(groupID, agentID string) error
CheckMembership verifies that the group exists and the agent is a member.
func (*GroupDMManager) ClearMessages ¶ added in v0.103.0
ClearMessages deletes the visible transcript for a live group without deleting the group itself. Pending notification buffers for the group are dropped so already-cleared messages are not delivered later.
func (*GroupDMManager) Create ¶
func (m *GroupDMManager) Create(name string, memberIDs []string, cooldown int, style GroupDMStyle, venue GroupDMVenue) (*GroupDM, error)
Create creates a new group DM with the given members. cooldown is the notification cooldown in seconds (0 = use default). style controls the communication style ("efficient" or "expressive"; empty = "efficient"). venue is the physical-setting hint ("chatroom" or "colocated"; empty = defaultGroupDMVenue).
func (*GroupDMManager) CreateThread ¶ added in v0.111.0
func (m *GroupDMManager) CreateThread(agentID string) (*GroupDM, error)
CreateThread always creates a NEW thread room (kind "thread") for the given agent — no member-set dedup, so an agent can have many parallel threads. The default name is DefaultThreadName ("無題のスレッド"); the first agent reply may auto-title it (see maybeAutoTitleThread).
func (*GroupDMManager) CreateWithNotify ¶ added in v0.103.0
func (m *GroupDMManager) CreateWithNotify(name string, memberIDs []string, cooldown int, style GroupDMStyle, venue GroupDMVenue, notifyMembers bool) (*GroupDM, error)
CreateWithNotify creates a group and optionally sends a system notification to every member after the row is persisted.
func (*GroupDMManager) DeadLetters ¶ added in v0.111.0
func (m *GroupDMManager) DeadLetters(ctx context.Context, groupID string, limit int) ([]*store.GroupDMDeadLetter, error)
DeadLetters lists permanently failed notification deliveries for a group.
func (*GroupDMManager) Delete ¶
func (m *GroupDMManager) Delete(id string, notify bool) error
Delete removes a group DM and its data. If notify is true, members are notified about the deletion before the group is removed.
func (*GroupDMManager) FindOrCreateDM ¶ added in v0.111.0
func (m *GroupDMManager) FindOrCreateDM(memberIDs []string) (*GroupDM, bool, error)
FindOrCreateDM returns the existing kind="dm" room whose member set equals memberIDs, or creates one. This is pure sugar over the group machinery: a DM is a normal room flagged kind "dm" so the UI can list it separately. One member means a human↔agent DM (the human operator is an implicit participant of every room); two members is an agent↔agent DM.
func (*GroupDMManager) Get ¶
func (m *GroupDMManager) Get(id string) (*GroupDM, bool)
Get returns a group DM by ID.
func (*GroupDMManager) GroupsForAgent ¶
func (m *GroupDMManager) GroupsForAgent(agentID string) []*GroupDM
GroupsForAgent returns all groups that contain the specified agent.
func (*GroupDMManager) LatestMessageID ¶ added in v0.15.0
func (m *GroupDMManager) LatestMessageID(groupID string) string
LatestMessageID returns the cached head message ID for a group, or "" if the group has no messages (or does not exist — the caller is expected to check existence separately when that distinction matters).
func (*GroupDMManager) LeaveGroup ¶ added in v0.8.1
func (m *GroupDMManager) LeaveGroup(id, agentID string) error
LeaveGroup removes an agent from a group DM voluntarily. The group is deleted if fewer than 2 members remain.
func (*GroupDMManager) List ¶
func (m *GroupDMManager) List() []*GroupDM
List returns all group DMs.
func (*GroupDMManager) LockPatch ¶ added in v0.101.0
func (m *GroupDMManager) LockPatch(groupID string) (release func())
LockPatch returns a per-group mutex acquired for the duration of an If-Match-gated PATCH (groupdm rename, member-settings updates). Mirrors Manager.LockPatch on the agent manager.
The returned function MUST be called to release; callers should `defer release()` immediately. Holding this lock across a precondition check + manager mutation + ETag echo trio closes the TOCTOU window inherent in reading the store etag at the handler boundary.
Cross-process / cross-device write coordination is the store layer's concern; LockPatch only serializes within one daemon process.
func (*GroupDMManager) MarkGroupRead ¶ added in v0.111.0
func (m *GroupDMManager) MarkGroupRead(groupID, messageID string) error
MarkGroupRead persists the operator's read cursor for a room at the seq of messageID. A stale/unknown messageID is ignored (no error) so a cursor pointing at a hard-deleted message doesn't wedge the badge. The store upsert never lowers an existing higher cursor, so out-of-order marks from concurrent devices are safe.
func (*GroupDMManager) Messages ¶
func (m *GroupDMManager) Messages(groupID string, limit int, before string) ([]*GroupMessage, bool, string, error)
Messages returns paginated messages for a group plus the current head ID of the transcript. The head ID is the cursor agents pass back as expectedLatestMessageId on a subsequent PostMessage to opt into the CAS guard against racing posts. "" means the group has no messages yet.
Both `messages` and `latestMessageId` are derived from the *same* on-disk read so the response is internally consistent — a concurrent PostMessage cannot leave us returning a head ID that is absent from the returned slice (or vice versa). The in-memory cache is intentionally not consulted here; it is solely a CAS-check accelerator for the post path, and reading it would re-introduce the two-snapshot race.
The existence check is performed twice — once before the read to fail fast on unknown IDs, once after to catch a Delete that ran while we were reading the file. Without the post-read recheck a freshly-deleted group would surface as `200 OK` with an empty messages list because loadGroupMessages turns "file not found" into an empty result.
func (*GroupDMManager) PostMessage ¶
func (m *GroupDMManager) PostMessage(ctx context.Context, groupID, agentID, content, expectedLatestID string, notify bool) (*GroupMessage, error)
PostMessage posts a message to a group and optionally notifies other members.
expectedLatestID, when non-empty, enables compare-and-set (CAS) guarding: if it does not match the current head of the transcript, the call is rejected with *StaleExpectedIDError carrying the new head and a capped diff of messages that arrived since the caller's cursor. This is how agents avoid replying to a thread that has already moved on. Empty expectedLatestID skips the check entirely (legacy/admin path).
If notify is true, other members receive a system notification in their 1:1 chat. Set notify=false for messages sent from notification-triggered chats to prevent loops. The reserved UserSenderID ("user") must go through PostUserMessage; calls with that agentID are rejected so no agent can impersonate a human-user message.
func (*GroupDMManager) PostMessageStrict ¶ added in v0.111.0
func (m *GroupDMManager) PostMessageStrict(ctx context.Context, groupID, agentID, content, expectedLatestID string, notify bool) (*GroupMessage, error)
PostMessageStrict is PostMessage with mandatory CAS: an empty expectedLatestID is rejected with *MissingExpectedIDError whenever the room already has a head. Used for agent-token API posts. The check runs under the same lock as the CAS compare so a racing first post cannot slip through the "room is empty" allowance.
func (*GroupDMManager) PostSteerMessage ¶ added in v0.112.0
func (m *GroupDMManager) PostSteerMessage(ctx context.Context, groupID, content string, attachments []MessageAttachment) (*GroupMessage, error)
PostSteerMessage posts an operator message into a thread room the same way PostUserMessage does (so it renders in order for every viewer) but never triggers runThreadTurn. Callers are expected to have already injected the text into the in-flight turn via Manager.SteerOneShot — this only handles the transcript/broadcast side.
func (*GroupDMManager) PostUserMessage ¶ added in v0.15.0
func (m *GroupDMManager) PostUserMessage(ctx context.Context, groupID, content string, attachments []MessageAttachment, notify bool) (*GroupMessage, error)
PostUserMessage posts a message from the human user (operator) to a group and notifies every member. Unlike PostMessage it bypasses membership checks because the human user is not a group member, and it never excludes anyone from the notification fan-out. CAS is intentionally not enforced for user posts: humans typing in the Web UI should not get 409s from the racing chatter of agents replying around them.
func (*GroupDMManager) RemoveAgent ¶
func (m *GroupDMManager) RemoveAgent(agentID string)
RemoveAgent removes an agent from all groups. Groups with fewer than 2 members are deleted.
Tombstone-first ordering: for each dissolved group we tombstone the DB row before draining m.groups so a failed SoftDelete doesn't leave the in-memory map and the DB out of sync (which would resurrect the group on the next restart). Member-shrink groups are persisted via persistOne; persist failures revert the in-memory mutation so the next save sweep doesn't propagate a phantom drop.
func (*GroupDMManager) Rename ¶ added in v0.8.0
func (m *GroupDMManager) Rename(id, name, callerAgentID string) (*GroupDM, error)
Rename changes the name of a group DM. Only members can rename.
func (*GroupDMManager) SetAPIBase ¶
func (m *GroupDMManager) SetAPIBase(base string)
SetAPIBase sets the base URL for agent-facing API docs in system prompts.
func (*GroupDMManager) SetCooldown ¶ added in v0.8.0
func (m *GroupDMManager) SetCooldown(id string, seconds int) (*GroupDM, error)
SetCooldown updates the notification cooldown for a group (in seconds).
func (*GroupDMManager) SetMemberNotifyMode ¶ added in v0.15.0
func (m *GroupDMManager) SetMemberNotifyMode(groupID, agentID string, mode NotifyMode, digestWindow int, callerAgentID string) (*GroupDM, error)
SetMemberNotifyMode updates a single member's notify mode and digest window. Muting a member cancels any pending buffer / timer so queued noise does not reach them after the switch.
callerAgentID, when non-empty, must be a current member of the group AND must be active (not archived / not deleted). Empty callerAgentID is the admin/UI path and skips the check, matching the SetStyle/SetVenue convention.
func (*GroupDMManager) SetOneShotForTesting ¶ added in v0.112.0
func (m *GroupDMManager) SetOneShotForTesting(fn func(ctx context.Context, agentID, userMessage string, opts OneShotOpts) (<-chan ChatEvent, error))
SetOneShotForTesting overrides the one-shot chat function used for thread turns. Exposed (exported) so tests outside this package — notably the HTTP handler tests in internal/server — can stub thread-turn behavior (including a blocking, event-emitting stub) without a real backend. Not used in production code paths.
func (*GroupDMManager) SetStyle ¶ added in v0.9.0
func (m *GroupDMManager) SetStyle(id string, style GroupDMStyle, callerAgentID string) (*GroupDM, error)
SetStyle updates the communication style for a group. callerAgentID must be a member. An empty callerAgentID skips the membership check (for admin/UI calls).
func (*GroupDMManager) SetVenue ¶ added in v0.15.0
func (m *GroupDMManager) SetVenue(id string, venue GroupDMVenue, callerAgentID string) (*GroupDM, error)
SetVenue updates the venue hint (chatroom / colocated) for a group. callerAgentID must be a member; empty skips the check (admin/UI). Mirrors SetStyle's auth convention so both group-wide settings flip the same way.
func (*GroupDMManager) Steer ¶ added in v0.112.0
func (m *GroupDMManager) Steer(ctx context.Context, groupID, content string) (*GroupMessage, error)
ThreadLive returns the live snapshot for a thread room's in-flight turn. active is false when no turn is currently running for groupID (idle, finished, or the room never had a thread turn) — the snapshot is then the zero value and should not be rendered. Steer injects an operator message into the currently running thread turn for groupID (see runThreadTurn / OneShotOpts.SessionKey), and posts it into the room transcript via PostSteerMessage so it renders inline for every viewer without spawning a second thread turn. Returns ErrAgentNotBusy if no thread turn is currently in flight for this room, or ErrSteerUnsupported if the agent's backend doesn't support steering.
func (*GroupDMManager) StopThreadTurn ¶ added in v0.116.2
func (m *GroupDMManager) StopThreadTurn(groupID string) bool
StopThreadTurn aborts the in-flight thread turn for a room on behalf of the operator (the thread UI's stop button). Unlike the archive-driven cancelThreadTurn, the turn's partial output is preserved: runThreadTurn posts whatever text/thinking/tool calls accumulated before the stop as an interrupted reply. Returns false when no turn is in flight.
func (*GroupDMManager) ThreadLive ¶ added in v0.112.0
func (m *GroupDMManager) ThreadLive(groupID string) (active bool, snapshot ThreadLiveSnapshot)
func (*GroupDMManager) UnreadInfo ¶ added in v0.111.0
func (m *GroupDMManager) UnreadInfo(groupID, afterID string, limit int, useOperatorCursor bool) (count int, mentionsUser, hasMore bool, err error)
UnreadInfo returns the number of messages strictly newer than afterID (the UI's per-room read cursor) plus whether any of them @-mentions the human operator. count is capped at limit (0 = 200); hasMore signals the cap bit. afterID=="" counts from the beginning of the transcript window.
useOperatorCursor folds the server-persisted per-room read cursor (the HUMAN operator's, written by MarkGroupRead) into the effective cursor. Pass true only for owner-principal requests — a member agent's unread position is its own client-supplied afterID and must not be advanced to wherever the human happens to have read.
func (*GroupDMManager) UpdateSettings ¶ added in v0.101.6
func (m *GroupDMManager) UpdateSettings(ctx context.Context, id string, patch GroupDMSettingsPatch) (*GroupDM, string, error)
UpdateSettings applies all supplied group-wide settings as one logical mutation. With a DB-backed manager it uses store.UpdateGroupDM's transaction and If-Match check, then mirrors the committed state into memory while still holding the group manager lock. Test fixtures without a store still get a single in-memory critical section.
type GroupDMSettingsPatch ¶ added in v0.101.6
type GroupDMSettingsPatch struct {
Name *string
Cooldown *int
Style *GroupDMStyle
Venue *GroupDMVenue
MaxHops *int
CallerAgentID string
IfMatchETag string
}
GroupDMSettingsPatch is an atomic update to group-wide settings. Nil fields are left unchanged.
type GroupDMStyle ¶ added in v0.9.0
type GroupDMStyle string
GroupDM represents a group conversation between agents. GroupDMStyle controls the communication style for a group conversation. "efficient" (default): concise, token-saving, no pleasantries. "expressive": human-like chat with greetings and conversational filler.
const ( GroupDMStyleEfficient GroupDMStyle = "efficient" GroupDMStyleExpressive GroupDMStyle = "expressive" )
type GroupDMVenue ¶ added in v0.15.0
type GroupDMVenue string
GroupDMVenue is the physical/virtual setting that hosts the conversation. Agents use this hint to calibrate speech style: a co-located venue invites references to shared surroundings and gestures, while a chat room constrains everything to the text channel.
- "chatroom" (default): closed online chat room. Members are not physically together; the only shared context is what is sent here.
- "colocated": same physical space. Members are co-present in real time and may reference ambient cues, gestures, deictic ('this', 'over there') language.
const ( GroupDMVenueChatroom GroupDMVenue = "chatroom" GroupDMVenueColocated GroupDMVenue = "colocated" )
type GroupMember ¶
type GroupMember struct {
AgentID string `json:"agentId"`
AgentName string `json:"agentName"`
// NotifyMode is the per-member delivery mode. Empty string is treated as
// NotifyRealtime on read but omitted from JSON to keep legacy groups small.
NotifyMode NotifyMode `json:"notifyMode,omitempty"`
// DigestWindow is the digest-batching window in seconds. Only meaningful
// when NotifyMode == NotifyDigest. 0 means "use defaultDigestWindow".
DigestWindow int `json:"digestWindow,omitempty"`
}
GroupMember is a participant in a group DM.
AgentName is a denormalized read-only display field. The DB store (members_json) does not persist it — it is rebuilt from the agents table on every load (and on every Members copy in groupdm_manager). Callers must not write AgentName expecting durability.
type GroupMessage ¶
type GroupMessage struct {
ID string `json:"id"`
AgentID string `json:"agentId"`
AgentName string `json:"agentName"`
Content string `json:"content"`
Attachments []MessageAttachment `json:"attachments,omitempty"`
// Hop is the agent-relay depth: 0 for user/system/fresh agent posts,
// trigger-hop + 1 for posts made from a notification-triggered turn.
Hop int `json:"hop,omitempty"`
// Mentions is the list of member ids (agent ids, or "user" for the
// human operator) referenced via @name in Content, parsed at post time.
Mentions []string `json:"mentions,omitempty"`
Timestamp string `json:"timestamp"`
// Usage is the token usage of an agent thread reply (nil otherwise).
Usage *Usage `json:"usage,omitempty"`
// Thinking is the extended-thinking text of an agent thread reply
// ("" otherwise).
Thinking string `json:"thinking,omitempty"`
// ToolUses is the tool-call trace of an agent thread reply (nil
// otherwise).
ToolUses []ToolUse `json:"toolUses,omitempty"`
// Model/Effort record the agent's configured model and effort level at
// the time of an agent thread reply ("" otherwise).
Model string `json:"model,omitempty"`
Effort string `json:"effort,omitempty"`
// Interrupted marks a thread reply whose turn ended before completion
// (operator stop, error, or timeout) — the content is partial output.
Interrupted bool `json:"interrupted,omitempty"`
}
GroupMessage is a single message in a group DM transcript.
type LlamaCppBackend ¶ added in v0.10.1
type LlamaCppBackend struct {
// contains filtered or unexported fields
}
LlamaCppBackend implements ChatBackend by talking directly to llama-server's OpenAI-compatible /v1/chat/completions endpoint via HTTP SSE streaming. No CLI dependency — just needs HTTP access to the server.
func NewLlamaCppBackend ¶ added in v0.10.1
func NewLlamaCppBackend(logger *slog.Logger) *LlamaCppBackend
func (*LlamaCppBackend) Available ¶ added in v0.10.1
func (b *LlamaCppBackend) Available() bool
func (*LlamaCppBackend) Chat ¶ added in v0.10.1
func (b *LlamaCppBackend) Chat(ctx context.Context, agent *Agent, userMessage string, systemPrompt string, opts ChatOptions) (<-chan ChatEvent, error)
func (*LlamaCppBackend) Name ¶ added in v0.10.1
func (b *LlamaCppBackend) Name() string
type Manager ¶
type Manager struct {
// OnChatDone is called when an agent finishes its response.
OnChatDone func(agent *Agent, message *Message)
// OnQuestionRaised is called once per AskUserQuestion prompt surfaced
// to the UI — NOT on every List() poll — mirroring OnChatDone. Wired
// by server.go to push an "agent_awaiting_input" web-push notification.
OnQuestionRaised func(agentID string)
// contains filtered or unexported fields
}
Manager manages agent CRUD, chat orchestration, and lifecycle.
func NewManager ¶
NewManager creates a new agent manager.
Returns an error only if kojo.db cannot be opened — that path is fatal for the daemon (without the store there is no agent metadata at all), so callers should bubble the error to main and exit. Per-agent load failures are logged and skipped, not returned, so a single corrupt row doesn't prevent the rest of the daemon from coming up.
func (*Manager) Abort ¶
Abort cancels any running chat for an agent — both the user chat (busy entry) AND any in-flight one-shot chats (Slack / group-DM responders). The §3.7 switch-device orchestrator relies on this dual cancel: WaitChatIdle's drain waits on both `busy` and `oneShotCancels`, so a bare Abort that only signaled the user chat would let one-shots run past the quiesce window and write transcript / JSONL after the snapshot.
cancelOneShots leaves the oneShotCancels map entry intact so waitOneShotClear / WaitChatIdle can observe completion as each goroutine removes itself via untrackOneShot. Idempotent — a second Abort on a finished chat is a no-op for both halves.
func (*Manager) AcquireMutation ¶ added in v0.101.0
AcquireMutation reserves a slot in the per-agent mutation counter and returns a release callback. The acquire fails when a §3.7 device switch is mid-flight on this peer. While the slot is held, WaitChatIdle observes the agent as non-idle — so Step -1's snapshot cannot race a mutation that started just before SetSwitching landed.
Common entry guard for every agent-state mutation surface that does NOT route through prepareChat (persona / settings / slackbot / tasks / credentials / avatar / slack tokens). The release callback is idempotent and safe to defer.
Threadsafe; nil-safe for hand-rolled test fixtures.
func (*Manager) ActivateAgentRuntime ¶ added in v0.101.0
ActivateAgentRuntime wires the in-memory cached *Agent into this peer's runtime side channels: the cron schedule. Called by the §3.7 device-switch finalize hook (NOT by phase-1 sync) so an agent that landed but hasn't yet been claimed by this peer can't fire schedules before AgentLockGuard adopts it.
Idempotent and threadsafe. Skipped (no-op) when StartSchedulers hasn't run yet — the boot loop registers everything from the same in-memory snapshot, so a redundant call here would just race.
Archived agents always have their cron entry torn down here, even when it was already detached by Archive(): a Reload that flipped an active agent to archived underneath us must still surface as "stopped" on the runtime side.
func (*Manager) ActiveTasksSummary ¶ added in v0.101.0
ActiveTasksSummary returns a formatted string of open tasks for system-prompt injection. Returns empty string if the agent has no open tasks. Task data is wrapped in a code fence to prevent prompt injection via task titles.
Errors from the store are treated as "no summary" — this is a best-effort prompt enrichment, not a correctness path. The caller already runs the agent without tasks if loadTasks fails in the v0 implementation, and this matches that semantics.
func (*Manager) AgentTokenStore ¶ added in v0.17.0
func (m *Manager) AgentTokenStore() AgentTokenStore
AgentTokenStore returns the wired-in token store (may be nil during tests or before SetTokenStore is called).
func (*Manager) AnswerQuestion ¶ added in v0.113.0
func (m *Manager) AnswerQuestion(ctx context.Context, agentID, requestID string, answers map[string]any, deny bool, denyMessage string) error
AnswerQuestion resolves a pending interactive AskUserQuestion raised by the agent's running turn (claude backend only — see ChatOptions.OnQuestionReady). On allow, answers maps each question string to the chosen answer; on deny the tool call is refused with denyMessage. Returns ErrAgentNotBusy when no turn is running (or the backend can't answer) and ErrQuestionNotFound when requestID doesn't match a pending question. On a successful allow it appends a readable user message recording the answers to the transcript (mirroring Steer) so the Q&A survives a reload.
func (*Manager) Archive ¶ added in v0.15.0
Archive marks an agent as archived without deleting most of its data.
All runtime activity is stopped: cron is unscheduled, in-flight one-shot chats are cancelled, the cached memory index is closed. The agent stays in the in-memory map (with Archived=true) and on disk — credentials, slack tokens, messages, memory, persona, avatar, tasks are all retained. Inbound chats route through prepareChat, which refuses archived agents with ErrAgentArchived.
EXCEPTION: group DM memberships are removed (and 2-person groups are dissolved). A dormant member silently sitting in a chat room is more confusing than useful, so we treat group membership as an "active state" that doesn't survive archive. Unarchive does NOT restore memberships — the agent must be re-invited.
The handler is responsible for stopping the Slack bot, since the bot lifecycle is owned by the server (slackHub), not the agent manager.
Idempotent: calling Archive on an already-archived agent is a no-op.
Takes Manager.LockPatch to serialize against in-flight PATCH-style mutations on the same agent. Lock order is LockPatch → acquireResetGuard → m.mu; PATCH handlers already take LockPatch first too.
func (*Manager) ArmRestartWake ¶ added in v0.111.0
ArmRestartWake persists the restart-wake marker for agentID. When sessionKey is non-empty the wake is routed back to that thread on boot (falling back to the main conversation if the thread is gone). Called by the restart drain goroutine right before the shutdown trigger fires — never earlier, so an aborted drain leaves no marker behind.
func (*Manager) BackendAvailability ¶ added in v0.10.0
BackendAvailability returns which agent backends are available.
func (*Manager) BlobStore ¶ added in v0.101.0
BlobStore returns the wired blob store (may be nil during tests or before SetBlobStore is called). Mirrors Store() / AgentTokenStore() for subsystem composition.
func (*Manager) BuildRecentMessagesContext ¶ added in v0.102.0
BuildRecentMessagesContext returns a bounded transcript excerpt for a fresh Claude session. It is best-effort: chat must still proceed when the DB read fails, because native --resume may be enough and this block is only fallback continuity.
func (*Manager) BuildVolatileContext ¶ added in v0.101.0
func (m *Manager) BuildVolatileContext(ctx context.Context, agentID string, queryContext string) string
BuildVolatileContext returns the per-turn context block prepended to a user message before it reaches the CLI backend. Everything that changes between turns belongs here, NOT in the system prompt — keeping it out of the system prompt is what lets Claude's prompt cache stay warm.
The block is wrapped in a `<context>...</context>` tag so the agent can recognise it as data, not instructions. Inner content is escaped so a stray `</context>` in a task title / diary entry / search snippet cannot close the outer tag and let authored data escape into instruction territory. The wrapper always carries at least the current `now: ...` line, so the return value is never empty.
queryContext is the search-results block from MemoryIndex.BuildContextFromQuery for the current user query. Pass "" when no index is available or the caller wants to skip query-based recall.
func (*Manager) BusySince ¶ added in v0.7.0
BusySince returns the time when the agent started its current chat. Returns zero time and false if the agent is not busy.
func (*Manager) CanResumeSession ¶ added in v0.20.0
CanResumeSession reports whether the next ChatOneShot for this (agentID, sessionKey) pair will actually resume an existing backend session. Three conditions must hold: the configured backend honors ChatOptions.SessionKey for resumption, the on-disk session artifact exists, AND the artifact is non-empty (a zero-byte JSONL is treated as "no session" because sessionFileUsable would delete it and start fresh on the next invocation anyway).
Used by the Slack bot to gate the "head+tail safety net" injection mode: on the first message in a thread we replay the full history, on subsequent messages we send only a small head+tail recap because the resumed session already carries the bulk of the conversation (see slackbot.Bot.sendToAgent and chathistory.FormatForInjectionHeadTail).
Claude caveat: this does NOT check sessionResetThresholdTokens. If a long-running thread's JSONL eventually exceeds the reset threshold, sessionFileUsable will summarise + delete it on the next chat and Claude starts a new session with --session-id. CanResumeSession would keep returning true (the file existed at probe time) so the resumed session loses prior Slack context. The head+tail safety net is the primary mitigation: even when this caveat fires, the next turn still re-injects the opening and most-recent slices of the thread, which re-seeds the new session with enough framing to continue. Replicating sessionFileUsable's token check here would either duplicate its destructive side effects (preReset summary, file removal) or require splitting it into a read-only probe; we accept that instead.
func (*Manager) CancelOneShotsForAgent ¶ added in v0.101.0
CancelOneShotsForAgent cancels every in-flight one-shot chat (Slack / group-DM responder) for the agent WITHOUT touching the busy entry. The §3.7 device-switch orchestrator calls this on the agent-self-call path where Abort()'s busy-cancel would kill the curl that initiated the switch. Pairs with WaitChatIdleSelfCall.
func (*Manager) Chat ¶
func (m *Manager) Chat(ctx context.Context, agentID string, userMessage string, role string, attachments []MessageAttachment, source ...BusySource) (<-chan ChatEvent, error)
Chat sends a message to an agent and returns a channel of streaming events. The role parameter controls how the input message is stored in the transcript ("user" for interactive chat, "system" for cron-triggered messages). An optional BusySource may be passed to tag the busy entry; defaults to BusySourceUser when omitted.
func (*Manager) ChatOneShot ¶ added in v0.10.0
func (m *Manager) ChatOneShot(ctx context.Context, agentID string, userMessage string, opts OneShotOpts) (<-chan ChatEvent, error)
ChatOneShot runs a one-shot chat that does not save to the agent's transcript (agent_messages). Used for external platform conversations (Slack, Discord) that carry their own context. Memory (MEMORY.md, diary) access is still available via system prompt.
With opts.SessionKey set AND a supporting backend (claude), the call resumes a key-derived Claude session so successive messages in the same conversation (e.g. Slack thread) share context. Otherwise the chat runs as a fresh ephemeral session each time, matching the legacy behaviour.
func (*Manager) Checkin ¶ added in v0.16.0
Checkin triggers a manual check-in for the agent. Unlike the periodic cron job, this does not acquire the cron lock and does not check the active-hours window — the user explicitly asked for it. The check-in runs asynchronously: events are drained in a background goroutine and the assistant reply is persisted to the transcript like any other chat.
Returns ErrAgentNotFound, ErrAgentArchived, or ErrAgentBusy on rejection.
func (*Manager) ClearAgentArrivedHere ¶ added in v0.101.0
ClearAgentArrivedHere removes the §3.7 target-arrival marker AND the sibling allowed_proxy_peer row. Called by the source-release path so a switch-away from this peer doesn't leave a stale arrival marker that would re-seed AgentLockGuard against an agent we no longer own. Idempotent; missing row on either key is not an error.
The proxy sibling is dropped best-effort AFTER the arrival marker goes — if the proxy delete fails the row becomes orphaned, but the boot restore path looks up by agent_id only when an arrival marker is present, so an orphaned proxy row is dead data the next MarkAgentArrivedHere will overwrite.
func (*Manager) ClearAgentReleasedHere ¶ added in v0.101.0
ClearAgentReleasedHere removes the §3.7 source-release marker. Called by the finalize hook on a re-handoff so the agent doesn't get evicted on the next restart. Idempotent; missing row is not an error.
func (*Manager) ClearAllEmbeddings ¶ added in v0.12.0
func (m *Manager) ClearAllEmbeddings()
ClearAllEmbeddings resets embeddings to NULL for all cached indexes. Called when the embedding model changes (dimensions may differ).
func (*Manager) Close ¶ added in v0.101.0
Close releases manager-owned resources (the kojo.db connection). Safe to call on a nil receiver and idempotent.
func (*Manager) CloseAllClaudeSessions ¶ added in v0.112.0
func (m *Manager) CloseAllClaudeSessions()
CloseAllClaudeSessions terminates all persistent claude processes.
func (*Manager) CloseAllIndexes ¶ added in v0.9.0
func (m *Manager) CloseAllIndexes()
CloseAllIndexes closes all cached MemoryIndex instances.
func (*Manager) CloseClaudeSession ¶ added in v0.112.0
CloseClaudeSession terminates the agent's persistent claude process, if any.
func (*Manager) CloseClaudeSessionSync ¶ added in v0.112.0
CloseClaudeSessionSync terminates the agent's persistent claude process and waits (bounded) for it to exit. Used by destructive paths that touch the session JSONL right after (e.g. the device-switch handoff).
func (*Manager) ConsumeRestartWake ¶ added in v0.111.0
ConsumeRestartWake reads and clears the restart-wake marker, then fires one system-role chat turn for the marked agent telling it the restart completed. Called once from main after the listeners are up (the system prompt embeds the agent API base, which is wired during listener setup). All failures are logged, never fatal — a lost wake just means the agent waits for the next human message, exactly the pre-wake behaviour.
func (*Manager) Create ¶
func (m *Manager) Create(cfg AgentConfig) (*Agent, error)
Create creates a new agent.
func (*Manager) CreateAgentMemoryEntry ¶ added in v0.101.0
func (m *Manager) CreateAgentMemoryEntry(ctx context.Context, agentID, kind, name, body string) (*MemoryEntryRecord, error)
CreateAgentMemoryEntry writes body to the canonical on-disk path for (kind, name) and inserts the matching memory_entries row. Same three-layer locking as PutAgentMemory: LockPatch → editing flag → memory-sync gate. Pre-syncs so a duplicate (kind, name) on disk surfaces as a conflict rather than a silent overwrite.
func (*Manager) CreateTask ¶ added in v0.101.0
func (m *Manager) CreateTask(ctx context.Context, agentID string, params TaskCreateParams) (*Task, error)
CreateTask adds a new task for an agent.
func (*Manager) Credentials ¶
func (m *Manager) Credentials() *CredentialStore
Credentials returns the credential store. Returns nil if the store failed to initialize.
func (*Manager) CronPaused ¶
CronPaused returns whether all cron jobs are globally paused.
func (*Manager) Delete ¶
Delete removes an agent and its data.
Takes Manager.LockPatch to serialize against in-flight PATCH-style mutations. Lock order: LockPatch → acquireResetGuard → m.mu.
func (*Manager) DeleteAgentMemory ¶ added in v0.101.0
DeleteAgentMemory removes the on-disk MEMORY.md and tombstones the DB row. Idempotent: a missing file with a tombstoned (or absent) row returns nil. ifMatchETag is checked against the live row when non-empty.
Same three-lock dance as PutAgentMemory (LockPatch → refuse-dormant → memorySyncMu) so concurrent lifecycle / sync ops can't race the disk-then-DB pair.
func (*Manager) DeleteAgentMemoryEntry ¶ added in v0.101.0
func (m *Manager) DeleteAgentMemoryEntry(ctx context.Context, agentID, entryID, ifMatchETag string) error
DeleteAgentMemoryEntry removes the on-disk file and tombstones the row. Idempotent when ifMatchETag is empty (a missing row returns nil); conditional with non-empty ifMatchETag surfaces ErrETagMismatch on missing/tombstoned rows so the caller can refetch.
func (*Manager) DeleteAllTasks ¶ added in v0.101.0
DeleteAllTasks hard-deletes every task row for agentID. Used by the data-reset flow (manager_lifecycle.Reset). Returns the number of rows removed, including tombstones — the operation vacates the agent's task slate regardless of liveness. Errors are logged and swallowed to match the v0 file-removal semantics, which silently ignored tasks.json missing.
func (*Manager) DeleteMessage ¶ added in v0.10.1
DeleteMessage removes a single message from the transcript. Only supported for the llama.cpp backend. Rejected with ErrAgentBusy while the agent has an active chat.
ifMatchETag forwards an optimistic-lock precondition to the store. Empty disables the check (back-compat for daemon-internal callers and the legacy unconditional UI path); HTTP handlers always pass through the value parsed from the If-Match header.
func (*Manager) DeleteMirrorForAgent ¶ added in v0.101.0
DeleteMirrorForAgent は agent の mirror 行を全削除する。 帰還 (holder==self になった瞬間) と ReleaseAgentLocally で呼ぶ。
func (*Manager) DeleteTask ¶ added in v0.101.0
DeleteTask removes a task by id (soft-delete tombstone). ifMatchETag is the strong etag from the client's If-Match header; pass "" for unconditional delete.
A task id that belongs to a different agent surfaces as ErrTaskNotFound (probe-protection); see UpdateTask for the rationale.
func (*Manager) Directory ¶
func (m *Manager) Directory() []DirectoryEntry
Directory returns minimal public info for all agents (for agent-to-agent discovery).
func (*Manager) DisarmRestartWake ¶ added in v0.111.0
func (m *Manager) DisarmRestartWake()
DisarmRestartWake best-effort clears the marker. Called when the restart trigger was refused (signal shutdown won the race — a stop, not a restart). Failures are logged only: the process is exiting and a leftover marker fires one benign turn on the next boot.
func (*Manager) DrainBlockers ¶ added in v0.114.0
DrainBlockers returns a sorted, human-readable list of everything that currently prevents the daemon from reaching the fully-idle state WaitAllChatsIdle waits for — one entry per blocking item with its kind and agentID. Empty slice means idle. Used by the restart drain logging, the timeout error, and the GET /api/v1/system/restart status endpoint so a stuck drain is diagnosable instead of silent.
Format examples:
busy:ag_x(source=notification,age=4m) preparing:ag_y(n=2) editing:ag_y resetting:ag_y mutating:ag_y(n=1) switching:ag_y notifying:ag_y(n=1) summarizing(n=1) oneShot:ag_z(id=7,key=groupdm:g_1,age=32m) profileGen:ag_w
func (*Manager) EvictNonLocalAgentsAtStartup ¶ added in v0.101.0
EvictNonLocalAgentsAtStartup drops in-memory side channels for any agent we previously §3.7-released as source. Used at daemon boot so a switched-away agent that left this peer doesn't resurrect: NewManager loads ALL persisted agent rows regardless of holder, and StartSchedulers would otherwise schedule cron jobs against them — landing writes on a peer that no longer owns the lock.
Eviction policy:
- Released marker absent (`handoff/released/<id>` kv row): KEEP. A bare `agent_locks.holder_peer != self` is ambiguous — it could mean (a) we used to host this agent and switched it away (evict), or (b) target's phase-1 sync landed and finalize hasn't fired yet (keep, AgentLockGuard + fencing middleware handle the in-flight state). Without a durable signal of (a) we cannot tell them apart.
- Released marker present: EVICT via detachAgentInMemory. We wrote the marker at source-release time; the agent's runtime belongs to the new holder.
- kv read error other than ErrNotFound: KEEP + log. A transient SQLite read failure shouldn't silently evict an agent this peer may still own; AgentLockGuard / fencing middleware will sort it out on subsequent ticks.
- Belt-and-suspenders: when the kv check says "evict", we also verify agent_locks.holder is somewhere other than self. If the marker is stale (e.g. operator-driven re-handoff that didn't clear it), refuse to evict and log loudly so the operator can reconcile rather than losing runtime for an agent this peer once again owns.
MUST be called BEFORE AgentLockGuard.Start (so the released agents' IDs aren't fed back into the guard's desired set) and BEFORE StartSchedulers (so cron never sees them). selfPeerID == "" is a no-op — the caller is single-peer / pre-§3.7 and there's nothing to filter against.
store nil is also a no-op (caller is responsible for wiring; we don't fail boot on missing store, m.store is the same handle but kept explicit so the caller can pass a different handle in test fixtures).
func (*Manager) Fork ¶ added in v0.11.0
func (m *Manager) Fork(srcID string, opts ForkOptions) (*Agent, error)
Fork creates a new agent by deep-copying the source agent's metadata and data files. Memory (MEMORY.md, memory/, persona, avatar) is always copied. The transcript (agent_messages) and its derived state — the on-disk memory index, the autosummary marker (kv: autosummary/<agentID>), and tasks (cloned via cloneAgentTasks against agent_tasks) — are copied only when IncludeTranscript is true. Tasks are cloned with fresh per-row ids so the destination's primary keys don't collide with the source's.
External integrations are intentionally NOT copied: SlackBot and credentials all require per-agent tokens that cannot be safely shared. CLI local state (.claude/) is also skipped so the fork starts a fresh session. WorkDir is cleared so the fork does not share external output storage with the source.
func (*Manager) GetAgentArrivedProxy ¶ added in v0.101.0
GetAgentArrivedProxy returns the allowed_proxy_peer override that MarkAgentArrivedHere stashed alongside the arrival marker. Used by the boot path to restore agent_locks.allowed_proxy_peer after a graceful-shutdown wipe + fresh AcquireAgentLock. Returns ("", nil) when no row exists — caller treats that as "leave fresh-acquire default" rather than erroring out (legacy arrivals predate this kv row).
func (*Manager) GetAgentMemory ¶ added in v0.101.0
GetAgentMemory returns the v1 store's view of MEMORY.md for agentID. The store row is the authoritative read path for cross-device readers — the on-disk file is the canonical write path for the local CLI but callers (Web UI, HTTP API) should consume the synced row.
store.ErrNotFound is returned when the row hasn't been synced yet (brand-new agent before ensureAgentDir, or post-DELETE tombstone). The handler maps that to 404.
func (*Manager) GetAgentMemoryEntry ¶ added in v0.101.0
func (m *Manager) GetAgentMemoryEntry(ctx context.Context, agentID, entryID string) (*MemoryEntryRecord, error)
GetAgentMemoryEntry returns a single entry by id. No pre-sync — see ListAgentMemoryEntries for the rationale. Refuses during ResetData so a caller doesn't see pre-reset rows from a tombstone- pending DB.
func (*Manager) GetAgentPersona ¶ added in v0.101.0
GetAgentPersona returns the v1 store's view of persona.md for agentID. The on-disk file remains canonical for the CLI (syncPersona reads it on every prepareChat); the DB row is the read path for cross-device consumers.
Returns store.ErrNotFound when no persona row has been synced yet (a brand-new agent that hasn't had upsertAgent run, or one whose row was tombstoned). Refuses with ErrAgentResetting during ResetData so a Web UI poll between the wipe + post-reset sync can't observe pre-reset content.
func (*Manager) GetRemote ¶ added in v0.101.0
GetRemote returns an agent whose runtime was released from the local manager via §3.7 device-switch but whose DB row still exists. HolderPeer is populated from agent_locks. Returns nil when the agent is either in-memory (use Get) or not in the DB.
func (*Manager) GetTask ¶ added in v0.101.0
GetTask returns a single task by id, validating it belongs to agentID. A task id that exists but belongs to a different agent surfaces as ErrTaskNotFound (not a probe-leak about which agent owns it) — same posture handleGetGroupDM uses for membership checks.
func (*Manager) HasCredentials ¶
HasCredentials returns true if the credential store is available.
func (*Manager) HasPendingQuestion ¶ added in v0.114.0
HasPendingQuestion reports whether agentID currently has an unanswered AskUserQuestion prompt outstanding. Folded into Agent.AwaitingAnswer by Manager.List.
func (*Manager) HydrateAgentBlobsAtLoad ¶ added in v0.101.0
func (m *Manager) HydrateAgentBlobsAtLoad()
HydrateAgentBlobsAtLoad is retained as a load-time cleanup hook for old deployments that previously materialized blob_refs into agentDir. It no longer copies generic blob_refs rows into the agent CWD: MEMORY.md + memory/ are DB-canonical and reconciled by the memory sync path, while avatar.* and chat attachments are served through the blob/message APIs rather than as CWD files.
Best-effort across agents: one agent's failure logs and the rest proceed.
Must be called AFTER SetBlobStore. cmd/kojo wires this in initAgents() right after SetBlobStore so the first Chat picks up a populated CWD.
func (*Manager) InFlightOneShotSessionKey ¶ added in v0.114.0
InFlightOneShotSessionKey returns the SessionKey of the agent's currently running one-shot (thread) turn, when exactly one non-empty key is in flight. Returns "" when the agent has no thread turn running, the running turns carry no SessionKey, or the choice is ambiguous (several distinct keys) — the caller then falls back to the agent's main conversation.
func (*Manager) IsAgentActive ¶ added in v0.18.0
IsAgentActive reports whether an agent is currently "active":
- Global cron is running (not paused)
- Agent is not archived
- Current time is NOT within the agent's silent hours
Returns (active, found). found is false when the agent ID is unknown.
func (*Manager) IsAgentDMAvailable ¶ added in v0.18.0
IsAgentDMAvailable reports whether an agent can receive DM notifications. Unlike IsAgentActive, this ignores global cron pause (DM delivery is independent of cron) and respects NotifyDuringSilent: an agent in silent hours is still "available" for DMs if they opted in.
Returns (available, found). found is false when the agent ID is unknown.
func (*Manager) IsBusyForStatus ¶ added in v0.18.1
IsBusyForStatus returns true only when the agent is busy with a user chat or cron job — automated notifications (group DM replies etc.) are excluded so that members don't all appear "busy" when responding to a broadcast notification.
func (*Manager) IsPrivileged ¶ added in v0.17.0
IsPrivileged returns whether the agent has the Privileged flag set. Used by auth.Resolver to map a per-agent token to RoleAgent vs RolePrivAgent.
func (*Manager) IsSwitching ¶ added in v0.101.0
IsSwitching returns true when SetSwitching(agentID, true) is in effect and a §3.7 device-switch is mid-flight.
func (*Manager) ListAgentMemoryEntries ¶ added in v0.101.0
func (m *Manager) ListAgentMemoryEntries(ctx context.Context, agentID string, opts MemoryEntryListOptions) ([]*MemoryEntryRecord, error)
ListAgentMemoryEntries returns the live entries for agentID.
Read-only path: does NOT pre-sync (file→DB). The daemon-side sync runs on Manager.Load + prepareChat + fork; mutation paths (Create/Update/Delete here) all pre-sync, so by the time a Web UI edits + reads back, the row is fresh. A caller observing a brief window between a CLI-side disk write and the next sync hook will see slightly stale data — acceptable for a read endpoint.
Refuses with ErrAgentResetting during ResetData so the caller doesn't observe pre-reset DB rows after the disk wipe but before the post-reset sync lands.
func (*Manager) ListArrivedAgents ¶ added in v0.101.0
ListArrivedAgents returns every agent_id whose LATEST handoff marker (by Value-encoded RFC3339Nano timestamp) is an arrival. "Latest wins" — rather than strict "released wins" — so a re-handoff back to this peer whose earlier ClearAgentReleasedHere transiently failed isn't permanently stranded: the new arrival's timestamp dominates the stale release marker even when the release marker is still on disk.
Used by --peer startup to seed AgentLockGuard.Start. Errors propagate so the caller can log a degraded seed.
func (*Manager) ListReleasedAgents ¶ added in v0.101.0
ListReleasedAgents returns every agent_id whose LATEST handoff marker is a release. Mirrors ListArrivedAgents's latest-wins semantic: a stale release marker that an outdated arrival preceded is NOT returned. Used by --peer startup to subtract "still-released" IDs from the agent_locks seed; an agent whose latest marker is arrival passes the filter even when a stale released/<id> row is still on disk.
func (*Manager) ListRemote ¶ added in v0.101.0
ListRemote returns agents that exist in the store but are NOT in the in-memory manager (i.e. their runtime was released via §3.7 device-switch). Each returned Agent has HolderPeer populated from agent_locks so the UI knows which peer hosts them. Returns nil when no remote agents exist or the store is unavailable.
func (*Manager) ListTasks ¶ added in v0.101.0
ListTasks returns all live tasks for an agent ordered by seq ASC.
func (*Manager) LockPatch ¶ added in v0.101.0
LockPatch returns a per-agent mutex acquired for the duration of an If-Match-gated mutation (currently only HTTP PATCH /api/v1/agents/{id}).
The returned function MUST be called to release the lock; callers should use `defer release()` immediately after acquiring. Holding this lock across a precondition-check + Update + ETag-echo trio closes the TOCTOU window inherent in reading the store etag outside m.mu (m.mu is released between Update's in-memory mutation and m.save()'s store write, so two concurrent same-etag PATCHes would otherwise both pass the precheck).
Cross-process / cross-device write coordination is the store-level optimistic-concurrency layer's job; LockPatch only serializes within one daemon process.
We never delete entries from patchMus: agent IDs are bounded (effectively low-tens to low-thousands per daemon) and a freed *sync.Mutex would be just as small, so the GC saving isn't worth the bookkeeping complexity of "which mutex is currently unheld and safe to drop".
func (*Manager) MarkAgentArrivedHere ¶ added in v0.101.0
MarkAgentArrivedHere persists the §3.7 target-arrival marker for agentID. Called by the finalize hook after token adopt + guard AddAgent succeed; the marker survives graceful shutdown (unlike agent_locks rows, which AgentLockGuard.Stop drops via ReleaseAgentLockByPeer) so a daemon restart can re-seed the guard's desired set. Idempotent; deadline-driven retry mirrors MarkAgentReleasedHere's durability story.
allowedProxyPeer carries the source device_id that finalize stamped onto agent_locks.allowed_proxy_peer. Stored in a sibling kv row so the boot path can re-stamp the column after the post-restart fresh AcquireAgentLock resets it to self. Pass empty when no proxy override is desired (test fixtures / legacy callers); the boot path treats missing rows as "leave fresh-acquire default."
func (*Manager) MarkAgentReleasedHere ¶ added in v0.101.0
MarkAgentReleasedHere persists the §3.7 source-release marker for agentID in the machine-scoped kv table. The value is the release timestamp (RFC3339); the value isn't consulted by the eviction path, only the row's existence is. Idempotent.
Deadline-driven retry: loops on PutKV failure with constant 100ms (markReleasedBackoff) sleeps between attempts until the caller's ctx expires. A single PutKV failure is almost always transient (SQLite BUSY under contention, brief disk pressure). The marker is critical: without it on disk, a daemon restart on the source side would resurrect schedulers / cron for an agent target now owns. The caller (releaseAgentLocallyCore) hands in a markReleasedCtxBudget-wide ctx so transient SQLite BUSY gets many shots inside that window.
func (*Manager) MessagesPaginated ¶
func (m *Manager) MessagesPaginated(agentID string, limit int, before string) ([]*Message, bool, error)
MessagesPaginated returns messages with cursor-based pagination.
func (*Manager) MirrorMessageExists ¶ added in v0.101.0
MirrorMessageExists は (agentID, messageID) が remote_message_mirror に 存在するかだけを返す。serveRemoteMessagesMerged が paginated 取得時の cursor 出所 source を切り分けるために使う。store 不在 / 読取失敗は false + nil で返す (失敗時は local fallback に倒すのが安全)。
func (*Manager) MirrorMessagesPaginated ¶ added in v0.101.0
func (m *Manager) MirrorMessagesPaginated(agentID string, limit int, before string) ([]*Message, bool, error)
MirrorMessagesPaginated は §3.7 device-switch で他 peer に転移している agent の remote_message_mirror から messages を読む。 holder peer が offline のときの read fallback として server 層が呼ぶ。 canonical な agent_messages とは独立 — Hub が直前に proxy 成功した時の スナップショット。MessagesPaginated と同じ envelope (oldest-first slice + hasMore) を返す。
func (*Manager) NextCronRun ¶ added in v0.16.0
NextCronRun returns the next *configured* run time for an agent, adjusted for silent hours. Returns the zero Time when the agent has no schedule, is archived, or doesn't exist.
Semantics: this is the schedule the cron entry would fire AT, NOT a guarantee the run will actually happen. Global cron-paused state is deliberately ignored here so the UI can render "what would be next" (suffixed with "(paused)" via the cronPausedGlobal indicator on the agent response). Callers that need "will-actually-fire" semantics must AND the return with !Manager.CronPaused() — the only such caller today is the runtime scheduler itself, which gates via runCronJob's CronPaused check before invoking Chat. The HTTP layer uses this for display only.
func (*Manager) NotifyDeviceSwitchArrival ¶ added in v0.101.0
func (m *Manager) NotifyDeviceSwitchArrival(agentID, sourcePeerName, opID string, notes ArrivalNotes)
NotifyDeviceSwitchArrival sends a system message to the agent so it can immediately resume work on this peer after a device switch. Runs async — the caller doesn't need to wait for the chat to finish. If the agent is already busy (e.g. a cron tick landed at the same instant) the system message is skipped silently.
Idempotent across finalize retries for the SAME opID; a fresh opID (a future switch back to this peer for the same agent) fires its own arrival chat. opID may be empty for legacy / test callers, in which case the dedup keys on agentID alone — accept that the retry-safety degrades to old behavior rather than impose an opID requirement on every caller. notes carries per-switch caveats (auto token re-issue, degraded flushes, transfer skips) appended to the arrival prompt.
func (*Manager) PruneToOwnedAgentsForPeer ¶ added in v0.101.0
PruneToOwnedAgentsForPeer は --peer mode 起動時に、NewManager が DB から 全行ロードした m.agents から「この peer が実際に所有している」ID 以外を detach する。所有判定は arrived markers ∪ agent_locks.holder == self、 マイナス released markers。これを呼ばないと StartSchedulers が 永続 agent 行全件に cron を載せ、他 peer と二重発火する。
EvictNonLocalAgentsAtStartup は released marker のついた行しか落とさ ないため、未finalize / orphan 行は残る。--peer は信頼できる "owned" 集合を arrived + lock holder から再構築できるので、Hub よりも厳しく pruning できる。
MUST be called AFTER EvictNonLocalAgentsAtStartup and BEFORE StartSchedulers。selfPeerID == "" / store nil / Manager nil は no-op で true を返す。
戻り値: ok=true は seed 構築成功 (部分的でも安全側に pruning できた)。 ok=false は seed 3 read のいずれかが err = m.agents から正当所有を 切り分けられない状態。呼出側は ok=false なら fail-closed として StartSchedulers の起動を skip する想定。
func (*Manager) PutAgentMemory ¶ added in v0.101.0
func (m *Manager) PutAgentMemory(ctx context.Context, agentID, body, ifMatchETag string) (*MemoryRecord, error)
PutAgentMemory writes body into the agent's MEMORY.md file and records the result in the v1 store. ifMatchETag is an optimistic- concurrency precondition checked against the v1 store's current row — empty means "create or replace, ungated". Returns the new MemoryRecord (with fresh etag) on success.
Locking story (in order):
- Manager.LockPatch(agentID) — serializes against PATCH-style mutations on the agent record (Archive / Delete / Unarchive all take this lock too). Without it a concurrent Archive could land between our archived-precheck and the file write, and we'd write to the dir of a now-dormant agent.
- resetting / busy guards via m.busyMu — refuses writes during a ResetData (which is itself wiping the file) so we don't race resurrection.
- memorySyncMu[agentID] — serializes against any concurrent daemon-side sync (Load / fork / prepareChat) so the file write and the DB upsert appear atomic to a cross-device reader.
The DB UPDATE goes through the singleton-only sync path (syncAgentMemoryToDB) — touching memory_entries here would conflate MEMORY.md updates with unrelated memory/*.md scan errors. After the disk write, we re-read the DB row to obtain the freshly-computed etag for the response.
Cross-device staleness: before checking If-Match we run a disk→DB sync so an unsynced CLI edit lands in the row first. Without that, a Web client holding an etag from a previous GET could PUT and silently clobber a CLI write that hadn't yet been synced.
func (*Manager) PutAgentPersona ¶ added in v0.101.0
func (m *Manager) PutAgentPersona(ctx context.Context, agentID, body, ifMatchETag string) (*PersonaRecord, error)
PutAgentPersona writes body to the agent's persona.md file and upserts the matching agent_persona row. ifMatchETag is the optimistic-concurrency precondition; "" means "ungated" (handlers MUST pass the client-supplied If-Match value).
Empty body is handled specially: writePersonaFile removes the file (the existing helper's contract — empty content = no persona). The DB row is still upserted with empty body so cross-device readers observe the cleared state.
Locking story (mirrors PutAgentMemory):
- Manager.LockPatch — serializes against PATCH /agents/{id} + Archive / Delete / Unarchive so a concurrent lifecycle op can't land between our DB precondition and the disk write.
- personaSyncMu — serializes against any concurrent daemon-side persona sync (Manager.save → upsertAgent) so the file write + DB upsert appear atomic to a cross-device reader.
We do NOT take m.editing here — persona writes are CLI-agnostic (the CLI process re-reads persona.md on the next prepareChat; no in-flight chat depends on the file staying stable). Refusing during ResetData is sufficient.
func (*Manager) RateLimit ¶ added in v0.114.0
func (m *Manager) RateLimit(agentID string) (RateLimitSnapshot, bool)
RateLimit returns the latest rate-limit snapshot for an agent. It checks the in-memory cache first, then lazily loads (and caches) the persisted snapshot from the kv table so a badge renders correctly after a restart before any new turn runs. ok is false when nothing has ever been recorded, or when the newest snapshot's window has already reset (stale telemetry) — in which case the stale in-memory and kv copies are lazily dropped so callers show nothing until fresh data lands.
func (*Manager) ReapplyHubLocalOverrides ¶ added in v0.111.0
func (m *Manager) ReapplyHubLocalOverrides(ctx context.Context, agentID string) ([]string, []string, error)
ReapplyHubLocalOverrides re-applies pending hub-local settings overrides on top of a just-ingested source-wins agents row (§3.7 peer_agent_sync). Called by the sync-ingest handler right after store.SyncAgentFromPeer commits.
Per-field 3-way merge (clock-free; see hubLocalOverrideRecord):
- no pending override → no-op
- ingested field == base → hub edit re-applied
- ingested field != base → holder changed it later; holder wins for that field (surfaced via the dropped list, never silent)
The kv row is cleared with the etag read at entry, so a concurrent PATCH's fresher override survives the delete and is honoured at the next sync. On apply error the kv row is kept and the error returned so the (idempotent) sync request can be retried.
Returns (appliedFields, droppedFields, err).
func (*Manager) Regenerate ¶ added in v0.10.1
Regenerate truncates the transcript at msgID and re-runs the associated user message through the llama.cpp backend. If msgID is an assistant message, msgID and all subsequent messages are removed and the preceding user message is re-sent. If msgID is a user message, all subsequent messages are removed and msgID itself is re-sent.
Returns after truncation and busy-slot registration so reloading clients can immediately see the in-progress chat. The backend request and event streaming run in the background; any backend.Chat failure is surfaced as an error event on the broadcaster (and persisted as a system message). Regenerate truncates and re-runs from msgID. ifMatchETag, when non-empty, preconditions on the clicked message's current etag inside the editing mutex (so a racing edit between the click and the truncate is caught before any rows are tombstoned). Empty disables the check (back-compat).
func (*Manager) ReleaseAgentLocally ¶ added in v0.101.0
ReleaseAgentLocally tears down every in-memory side channel that could fire local writes for the agent on THIS peer: cron schedule, in-flight one-shots, cached memory index, group-DM notification timers, and the agents map entry itself. The on-disk DB rows, durable group-DM memberships, persona, and blob bodies remain so a future agent-sync back to this peer can re-hydrate via ReloadAgentFromStore.
Called by the §3.7 source-release hook after a successful complete + finalize: target now holds agent_locks, and source must stop firing cron / internal Chat against this agentID. Without this teardown, source's cron scheduler keeps the entry and runCronJob proceeds through the agents.Get hit; the resulting Chat would write transcript / JSONL that target never sees.
Safe to call multiple times — every step is no-op-on-missing. Threadsafe.
func (*Manager) ReloadAgentFromStore ¶ added in v0.101.0
ReloadAgentFromStore re-reads the agent row from kojo.db and installs (or replaces) the in-memory cached *Agent. Used by the §3.7 device-switch agent-sync hook so a peer-pushed row becomes immediately visible to chat / list / fork without a daemon restart.
Caller MUST have already committed the row via store.SyncAgentFromPeer (or equivalent) before calling. A store.ErrNotFound result here is surfaced verbatim so the hook can decide whether to log + skip or refuse the sync.
func (*Manager) ReplaceMirrorWindowFromMessages ¶ added in v0.111.0
func (m *Manager) ReplaceMirrorWindowFromMessages(agentID, holderPeer string, msgs []*Message) error
ReplaceMirrorWindowFromMessages は holder の「最新 N 件スナップショット」 (cursor なし GET ?limit=N の応答) で mirror の該当 window を置き換える。 window 内にあるのに holder が返さなかった行 = holder 側で削除された message を prune する。msgs が空 = holder transcript が空 (呼出側で hasMore=false を確認済み) のとき mirror を全消しする。holder check は UpsertMirrorFromMessages と同じく store 層の単一 tx 内で行われる。 背景 refresher (mirror_refresher.go) 専用 — paginated proxy GET には window 置換の意味論が合わないので UpsertMirrorFromMessages を使うこと。
func (*Manager) ResetData ¶
ResetData removes conversation logs and memory but keeps settings, persona, avatar, and credentials.
func (*Manager) ResetSession ¶ added in v0.9.0
ResetSession clears the CLI session (e.g. Claude JSONL) for an agent without deleting conversation history or memory. The next chat will start a fresh CLI session with the full system prompt re-injected.
func (*Manager) SetAttachmentForwarder ¶ added in v0.101.0
func (m *Manager) SetAttachmentForwarder(f AttachmentForwarder)
SetAttachmentForwarder wires the hub-push callback. cmd/kojo calls this from --peer mode after the peer identity is loaded; leaving it nil (hub mode) is the correct posture there because the local Put IS the canonical copy.
func (*Manager) SetBlobStore ¶ added in v0.101.0
SetBlobStore wires the native blob store handle. Called post- construction by cmd/kojo after the blob.Store is built (it depends on agentMgr.Store(), so the order is Manager → store → blob.Store → SetBlobStore). Tests that don't exercise avatar I/O may leave it nil; avatar read paths fall back to the generated-SVG branch when blobStore is nil so a Get on an agent in those tests still completes.
func (*Manager) SetCronPaused ¶
SetCronPaused persists the global cron pause state and, on success, updates the in-memory flag. Returns an error if the store rejected the write so the HTTP handler can surface it as 5xx — acknowledging a toggle that hasn't been persisted would let a transient kv-write failure desync the UI from the truth across a restart.
Concurrency: cronToggleMu serializes the (memory ↔ kv) update so two concurrent toggles can't interleave their kv-writes and memory-writes in opposite orders and leave the two stores disagreeing.
Ordering policy is asymmetric — biased toward "fail closed" in flight:
paused == true (operator pausing): set memory FIRST so any cron tick happening during the kv write sees the pause immediately. If the kv write fails we revert memory back to false and surface the error; the operator's request didn't land but at most one in-flight tick saw the pause briefly (harmless: paused never starts work, only blocks new work).
paused == false (operator resuming): persist FIRST so we never advertise "running" until the kv row actually says so. A kv write failure leaves both memory and kv at "paused" — the operator's request fails closed.
In both branches the post-condition on success is (memory == kv == requested value). On failure: (memory == kv == previous value).
func (*Manager) SetGroupDMManager ¶
func (m *Manager) SetGroupDMManager(gdm *GroupDMManager)
SetGroupDMManager sets the group DM manager reference. Called after both managers are created to avoid circular dependency.
func (*Manager) SetPrivileged ¶ added in v0.17.0
SetPrivileged toggles the Privileged flag on the named agent and persists the change. Owner-only mutation enforced at the API layer.
func (*Manager) SetQuiescing ¶ added in v0.111.0
SetQuiescing flips the daemon-wide chat gate. When on, acquirePreparing refuses every new Chat / ChatOneShot with ErrAgentBusy ("server restart in progress") for ALL agents. In-flight turns are unaffected — the restart path waits for them via WaitAllChatsIdle. Idempotent; guard-off on a non-quiescing manager is a no-op.
func (*Manager) SetSwitching ¶ added in v0.101.0
SetSwitching marks the agent as mid-§3.7-switch (true) or clears the marker (false). When set, IsSwitching returns true and the chat path refuses new starts with ErrAgentSwitching so no transcript / JSONL is written between Step -1's quiesce and the post-complete drain. Idempotent: setting true on an already-switching agent is a no-op; clearing an agent that wasn't switching is also a no-op.
Returns ErrAgentBusy when on=true and a restart drain is quiescing the daemon — the quiesce check and the switching set share busyMu, so a switch can never start after WaitAllChatsIdle observed idle. Clearing (on=false) always succeeds so the orchestrator's deferred cleanup can't be refused.
func (*Manager) SetTokenStore ¶ added in v0.17.0
func (m *Manager) SetTokenStore(ts AgentTokenStore)
SetTokenStore wires the per-agent token store. Calling this after agents have been loaded triggers token bootstrap for each existing agent (so a kojo upgrade gets every agent a token without an explicit migration step).
func (*Manager) Shutdown ¶
func (m *Manager) Shutdown()
Shutdown stops all cron jobs and cancels active chats.
Short-circuits when StartSchedulers never ran (e.g. an early-exit CLI subcommand path that built a Manager but didn't reach the post-hydrate scheduler boot).
func (*Manager) SnapshotAccumulatedMessageRecord ¶ added in v0.101.0
func (m *Manager) SnapshotAccumulatedMessageRecord(agentID string) *store.MessageRecord
SnapshotAccumulatedMessageRecord reconstructs the in-flight assistant message from the busy entry's shared accumulator and returns it as a store.MessageRecord ready for inclusion in the §3.7 agent-sync payload.
Used by the device-switch self-call path: the assistant turn containing the kojo-switch-device tool_use is still mid-flight (accumulated in processChatEvents' local variables, not yet persisted to the messages table). Without this snapshot the sync payload would miss the last assistant turn entirely, and the §3.7 release guard would prevent the processChatEvents defer from ever persisting it.
Reads the shared chatAccumulator (NOT the chat broadcaster's log) because outCh's non-blocking send drops non-terminal events under back-pressure — so the broadcaster's log silently misses streaming text / tool_use rows on long claude turns and the snapshot would migrate a partial message. The accumulator is fed inline by processChatEvents on every event, regardless of outCh pressure, so a Snapshot read here matches the chat goroutine's own view of the turn.
Falls back to the broadcaster's log when the busy entry has no accumulator (legacy / hand-rolled test fixtures); the legacy path retains the original behavior so existing tests don't regress.
Returns nil if the agent is not busy or no streaming data has accumulated. The caller appends the returned record to the sync payload WITHOUT persisting it to the source's DB — on abort the chat continues normally and the done event persists the full message; on success the source is released and persistence is moot.
func (*Manager) StartFileWatcher ¶ added in v0.101.3
func (m *Manager) StartFileWatcher()
StartFileWatcher launches the agent-file → DB reflection watcher. Idempotent and best-effort: a watcher init failure is logged and the daemon falls back to the lazy prepareChat sync. Call once after agents are loaded (the watcher walks existing agent dirs at start; new ones are picked up via CREATE events).
func (*Manager) StartSchedulers ¶ added in v0.101.0
func (m *Manager) StartSchedulers()
StartSchedulers boots the cron loop and schedules every non-archived agent's interval. Split out of NewManager (Phase G follow-up to codex review) so cmd/kojo can interpose load-time cleanup between agent load and scheduler start. The generic blob hydrate path no longer populates agent CWDs; memory/workspace files are reconciled by their typed DB sync paths instead.
Idempotent: a sync.Once guard pins the boot to a single execution regardless of how many callers race here. Shutdown short-circuits when StartSchedulers never ran.
func (*Manager) SteerOneShot ¶ added in v0.112.0
SteerOneShot injects an additional user message into an in-flight ChatOneShot turn keyed by OneShotOpts.SessionKey (thread/Slack turns — see runThreadTurn). Returns ErrAgentNotBusy if no matching turn is running, and ErrSteerUnsupported if the turn's backend doesn't support steering.
func (*Manager) Store ¶ added in v0.101.0
Store returns the underlying *store.Store so subsystems built on the agent manager (groupdm manager, follow-up cutover slices) can share the connection rather than each opening their own *sql.DB. May be nil in tests that constructed a *Manager via &Manager{}.
func (*Manager) Subscribe ¶ added in v0.7.0
func (m *Manager) Subscribe(agentID string) (startedAt time.Time, past []ChatEvent, live <-chan ChatEvent, unsub func(), busy bool)
Subscribe returns a snapshot of all past events and a live channel for an agent's ongoing chat. The caller must call unsub when done to free resources. If the agent is not busy, busy is false and all other values are zero.
func (*Manager) TeardownAgentRuntime ¶ added in v0.101.0
TeardownAgentRuntime stops every in-memory runtime side channel (cron, slack, in-flight one-shots, agents map entry) WITHOUT writing the §3.7 source-release marker and WITHOUT touching DB rows. Used by the inter-peer state-probe self-heal path: the orchestrator is retrying a device-switch against this host, so this peer has to stop driving the agent before the new agent-sync lands, but the released marker would flag the row for startup eviction on the next boot and undo the retry.
Idempotent. Threadsafe.
func (*Manager) TruncateMemoryAt ¶ added in v0.20.0
TruncateMemoryAt removes everything in the agent's memory recorded at or after `since`, leaving anything strictly before it untouched. Comparison is "timestamp >= since" — the boundary record is dropped, not kept.
Targets:
- agent_messages rows for this agent (kojo transcript) — tombstoned via Store.TruncateMessagesFromCreatedAt.
- ~/.claude/projects/<encoded(agentDir)>/<sessionID>.jsonl (Claude --resume state). After the timestamp filter, trailing assistant / tool_result entries are also dropped so the file ends on a real user message — otherwise --resume on a half-finished turn breaks claude. A file that would be left empty is removed entirely.
- <agentDir>/memory/YYYY-MM-DD.md (daily diary) AND the matching memory_entries rows. Files whose date is strictly after since's JST date are deleted (and their DB row tombstoned); the file matching since's JST date keeps only `- HH:MM — ...` bullets timestamped before since's HH:MM (with the DB body updated to match).
Untouched: persona, MEMORY.md, memory/projects/*, memory/people/*, memory/topics/*, memory/archive/*, credentials, tasks, agent settings.
Acquires the same reset guard as ResetData and waits for any in-flight chat — including one-shot chats (Slack / Discord / Group DM), which acquireResetGuard alone does NOT cancel — to finish, so cron / notify pollers see ErrAgentResetting in the meantime instead of racing the rewrites against memory writes.
func (*Manager) TruncateMemoryFromMessage ¶ added in v0.20.0
func (m *Manager) TruncateMemoryFromMessage(agentID, msgID string) (*TruncateMemoryResult, error)
TruncateMemoryFromMessage truncates the agent's memory using the message identified by msgID. The matched message and everything sequentially after it in the kojo transcript are removed by seq (not timestamp), so two messages that share an RFC3339-second timestamp are not over-deleted; the matched message's timestamp is still used as the threshold for Claude session JSONL records and daily diary bullets, where positional equivalents are not available.
Returns ErrMessageNotFound if no such message exists.
func (*Manager) Unarchive ¶ added in v0.15.0
Unarchive restores a previously archived agent: clears the Archived flag and re-schedules its cron. Slack bot re-arming is the handler's responsibility (server owns slackHub lifecycle).
Idempotent: calling Unarchive on a non-archived agent is a no-op.
Serializes against Archive via acquireResetGuard so an Archive/Unarchive race can't interleave flag flips with cron operations. The idempotency check happens INSIDE the guard so we can't observe a stale "not archived" state while a concurrent Archive is mid-flight (the guard blocks until that Archive's cleanup() releases it).
func (*Manager) Update ¶
func (m *Manager) Update(id string, cfg AgentUpdateConfig) (*Agent, error)
Update updates an agent's configuration. Only non-nil fields are applied.
Validation order (codex review): every cfg field with a Valid* predicate is checked BEFORE any side-effecting write — disk writes (persona.md), avatar fetches, in-memory mutations. A malformed PATCH payload (effort, model+effort combo, workDir, intervalMinutes, timeoutMinutes, silentHours, etc.) returns its error with zero side effects. Without this barrier, a payload that set Persona="..." plus an invalid Effort would land the persona write to disk + DB, then fail on Effort, leaving the caller with a half-applied PATCH whose visible state diverges from the response code.
func (*Manager) UpdateAgentMemoryEntry ¶ added in v0.101.0
func (m *Manager) UpdateAgentMemoryEntry(ctx context.Context, agentID, entryID, ifMatchETag string, patch MemoryEntryPatch) (*MemoryEntryRecord, error)
UpdateAgentMemoryEntry applies a body-only patch to the entry. If the patch attempts to change kind or name, we refuse with ErrMemoryEntryRenameUnsupported — making rename crash-safe against disk + DB without an intent-file protocol is genuinely hard, and callers that need rename can DELETE + CREATE.
If-Match is enforced against the freshly-synced view, and again inside the store TX (defense in depth).
func (*Manager) UpdateMessageContent ¶ added in v0.10.1
func (m *Manager) UpdateMessageContent(agentID, msgID, content, ifMatchETag string) (*Message, string, error)
UpdateMessageContent replaces the content of a single message in the transcript. Only supported for the llama.cpp backend. Rejected with ErrAgentBusy while the agent has an active chat.
ifMatchETag is forwarded to the store layer so the optimistic- concurrency check is atomic with the UPDATE. Empty value skips the check (used by daemon-internal callers — currently none, but keeps the door open for tools that mutate transcripts without a client etag).
Returns (msg, newETag, err). The etag is the value computed inside the same transaction as the UPDATE, so handlers can echo it as an HTTP ETag without a follow-up read that would race against any edit landing after acquireTranscriptEdit's release.
func (*Manager) UpdateRemoteHubRow ¶ added in v0.111.0
func (m *Manager) UpdateRemoteHubRow(id string, cfg AgentUpdateConfig, casIfMatch string) (*Agent, error)
UpdateRemoteHubRow applies a hub-local-safe PATCH to the hub's own agents row for an agent whose runtime is held by a remote peer. Used by the offline-holder fallback in remoteAgentProxyMiddleware; the online-holder path keeps proxying so the holder's in-memory agent stays authoritative.
Consistency contract: there is no continuous agents-row sync between hub and holder — the row travels only inside the §3.7 peer_agent_sync payload at device-switch time, and that ingest is source-wins. A value written here is immediately visible on every hub read (GET / list go through GetRemote → hub row). To keep an acknowledged 200 from being SILENTLY clobbered by that source-wins ingest, every successful write also records a pending override (kv row; recordHubLocalOverride) which the sync-ingest handler re-applies on top of the incoming row via ReapplyHubLocalOverrides — unless the incoming row is newer than the hub write, in which case the holder-side edit is the user's later intent and legitimately wins.
No AcquireMutation here: the agent is not in the local in-memory map, so there is no local runtime state to fence. Per-agent HTTP serialization (LockPatch) is the caller's job, same as handleUpdateAgent. casIfMatch, when non-empty, is passed to store.UpdateAgent as the row-etag precondition so the If-Match check is atomic with the UPDATE (stronger than handleUpdateAgent's precheck-then-write); mismatch surfaces as store.ErrETagMismatch.
func (*Manager) UpdateSlackBot ¶ added in v0.10.0
func (m *Manager) UpdateSlackBot(id string, cfg *SlackBotConfig) error
UpdateSlackBot updates the Slack bot configuration for an agent. Pass nil to remove the configuration.
func (*Manager) UpdateSlackBotAlreadyGuarded ¶ added in v0.101.0
func (m *Manager) UpdateSlackBotAlreadyGuarded(id string, cfg *SlackBotConfig) error
UpdateSlackBotAlreadyGuarded is the no-guard variant of UpdateSlackBot for callers that already hold AcquireMutation for the agent (e.g. handleSetSlackBot / handleDeleteSlackBot, which wrap token Save + UpdateSlackBot + hub Reconfigure in one outer mutation hold). Calling UpdateSlackBot from inside the outer hold races with SetSwitching: switching can flip to true between the outer Acquire and the inner Acquire, leaving the token saved but the config row unwritten. This variant skips the inner guard so the whole handler is one transactional unit under the outer mutation.
MUST NOT be called from any path that does NOT already hold AcquireMutation for this agent.
func (*Manager) UpdateTask ¶ added in v0.101.0
func (m *Manager) UpdateTask(ctx context.Context, agentID, taskID, ifMatchETag string, params TaskUpdateParams) (*Task, error)
UpdateTask updates a task by id. ifMatchETag is the strong etag from the client's If-Match header; pass "" for unconditional update.
Translation: "open" → "pending", "done" → "done". Empty string in params.Status is rejected at the boundary because the JSON "status: null" would already arrive as a nil pointer; an explicit "status: \"\"" almost certainly indicates a client bug rather than a request to clear the field.
func (*Manager) UpsertMirrorFromMessages ¶ added in v0.101.0
UpsertMirrorFromMessages は proxy 経由で peer から取得した messages を Hub の remote_message_mirror に upsert する。message_id で dedup される。 holder peer が offline になった瞬間でも、直前の成功 proxy 分は mirror に 残るため、read fallback でそのまま返せる。
func (*Manager) WaitAllChatsIdle ¶ added in v0.111.0
WaitAllChatsIdle polls until every concurrent write path across ALL agents has drained, or ctx is cancelled. The daemon-wide analogue of WaitChatIdle — same six checks, plus `switching` (a §3.7 device switch must not be cut in half by a re-exec) and the post-turn summarize counter (turnSummarizeAsync runs after busy clears, and killing it mid-write would corrupt recent.md / the cursor file).
Callers should SetQuiescing(true) first; otherwise a fresh turn can start between the idle observation and whatever the caller does next (the restart path re-execs the process).
func (*Manager) WaitChatDone ¶ added in v0.101.0
WaitChatDone blocks until the busy entry's chat goroutine emits a terminal `done` event for the agent's in-flight turn or ctx is cancelled. Returns the captured assistant Message converted to a store.MessageRecord (with seq=0; caller stamps the target-side allocation, ToolUses cleared — see below) or nil on ctx cancellation / missing busy entry / nil accumulator.
ToolUses is intentionally CLEARED on the returned record. The done event carries the WHOLE turn — the agent's pre-tool-call text, every tool_use (including the kojo-switch-device call that triggered this whole flow), and every tool_result. The snapshot row the orchestrator shipped during /agent-sync ALREADY carries that tool_use; if the tail row carried it too, target's userMessageAddressed / planATailContent would mis-classify the tail as another snapshot (substring-detect of `kojo-switch-device` in ToolUses) and the commitment text would not surface in the arrival prompt. Stripping ToolUses leaves the tail as a pure "post-tool-result completion text" row, distinct in shape from the snapshot.
Used by the device-switch self-call orchestrator's deferred finalize: after /handoff/complete moves the lock to target, the source's claude turn continues for a few hundred milliseconds to emit a post-tool-result "I'll do X on arrival" message. Without this wait the orchestrator would race ahead, ship /handoff/finalize to target empty-handed, and target's arrival prompt would fire against a transcript missing the agent's own commitment text. The caller passes a bounded ctx so a wedged source backend can't indefinitely defer target activation; on timeout the orchestrator ships finalize WITHOUT the tail (current behavior — degraded but not stuck).
Snapshots the accumulator pointer under busyMu so a concurrent clearBusy(agentID) that removes the entry doesn't strand the caller on a dead reference. The accumulator outlives clearBusy (closed channel + capture stays live until the wait returns).
func (*Manager) WaitChatIdle ¶ added in v0.101.0
WaitChatIdle polls busyMu until every concurrent write path has drained for the agent OR ctx is cancelled. Returns nil on idle, ctx.Err() on timeout. Caller is responsible for issuing Abort first (WaitChatIdle just observes flags; without an abort it would block until the chat finishes naturally).
Drains:
- busy: in-flight Chat / ChatOneShot
- preparing: a Chat between prepareChat entry and busy entry insert (disk side effects still landing)
- editing: Regenerate / transcript edit holding the acquireTranscriptEdit guard
- resetting: ResetData / Fork / Archive / ResetSession holding the acquireResetGuard
- mutating: non-chat state writers (persona / settings / task / credential / avatar / slack tokens) holding AcquireMutation
- oneShotCancels: Slack / group-DM one-shot chats cancelled by switch_device_handler's Abort
Without all six checks the §3.7 quiesce window would race a Slack / cron / persona-edit write that landed mid-handoff.
Used by the §3.7 device-switch orchestrator: after Abort the chat goroutine still needs a few hundred ms to flush its final claude session JSONL append before we snapshot the file. The 1.5 s caller default is generous; in practice typical aborts drain in well under 100 ms.
func (*Manager) WaitChatIdleSelfCall ¶ added in v0.101.0
WaitChatIdleSelfCall is the §3.7 device-switch variant used when the HTTP request is the agent's own chat tool — typically the kojo-switch-device skill's curl. That curl is driven by the busy entry it would otherwise wait on, so the busy check would deadlock until the orchestration context timed out. Skipping busy lets every OTHER concurrent writer (preparing, Slack one-shots, editing, resetting, mutating, profileGen) still drain before the snapshot.
preparing is intentionally NOT skipped: prepareChat exits before busy is set, so the self chat itself no longer counts; a non-zero preparing counter means a DIFFERENT chat is in prepareChat and must be drained.
Pair with CancelOneShotsForAgent (not Abort) on entry so we don't cancel the busy entry making the call.
func (*Manager) WakeChat ¶ added in v0.111.0
WakeChat fires an asynchronous system-role chat turn with a caller-supplied prompt. Mirrors Checkin's plumbing (timeout from the agent's TimeoutMinutes, background event drain, transcript persistence via the normal Chat path) but with an arbitrary message instead of checkin.md.
func (*Manager) WakeChatThread ¶ added in v0.114.0
WakeChatThread delivers a system-role turn to the thread named by sessionKey (falling back to the main conversation when the thread is gone or sessionKey is empty), exactly like a restart-wake but with a caller-supplied message. The restart handler uses it to surface a drain-abort notice into the wake target's transcript.
func (*Manager) WakeThread ¶ added in v0.114.0
WakeThread delivers the wake turn into a group-DM thread room. Only the "groupdm:<id>" session-key flavour has a boot-time registry that can be resolved and re-driven; anything else (e.g. a Slack thread key, which needs the slackbot to redeliver) returns errWakeThreadGone so fireWake falls back to the main conversation. Delivery reuses the normal thread post path (PostUserMessage) so the wake lands in the thread transcript and triggers the agent turn exactly like a human message would.
func (*Manager) WatchChatStart ¶ added in v0.10.0
WatchChatStart returns a channel that receives a signal whenever a new chat starts for the given agent. Call the returned function to unsubscribe.
type MemoryEntryListOptions ¶ added in v0.101.0
type MemoryEntryListOptions struct {
Kind string // "" = all kinds; otherwise must be a valid kind
Limit int // 0 = unbounded (caller-side; server pages at 200/500)
Cursor int64 // seq strictly greater (keyset pagination)
}
MemoryEntryListOptions exposes the store's pagination knobs to callers without leaking the store package.
type MemoryEntryPatch ¶ added in v0.101.0
MemoryEntryPatch is the partial-update shape for PATCH. Pass nil to leave a field unchanged.
Kind / Name pointers exist on the type so callers can report 400 for "rename not supported" cleanly — UpdateAgentMemoryEntry rejects any non-nil Kind / Name with ErrMemoryEntryRenameUnsupported rather than silently dropping. Body-only patches are the supported path; rename = DELETE + CREATE.
type MemoryEntryRecord ¶ added in v0.101.0
type MemoryEntryRecord struct {
ID string
AgentID string
Seq int64
Kind string
Name string
Body string
ETag string
CreatedAt int64
UpdatedAt int64
DeletedAt *int64
}
MemoryEntryRecord is the public representation of a single memory_entries row for HTTP / Web UI consumers. Mirrors the store shape but doesn't leak the package boundary.
type MemoryIndex ¶
type MemoryIndex struct {
// contains filtered or unexported fields
}
MemoryIndex provides FTS5-based keyword search across agent memory.
func OpenMemoryIndex ¶
func OpenMemoryIndex(agentID string, logger *slog.Logger, creds *CredentialStore) (*MemoryIndex, error)
OpenMemoryIndex opens or creates the FTS5 index for an agent.
func (*MemoryIndex) BuildContextFromQuery ¶
func (idx *MemoryIndex) BuildContextFromQuery(query string) string
BuildContextFromQuery searches the index and returns formatted context for injection into system prompt. Uses hybrid search (FTS5 + vector) when embeddings are available, falls back to FTS5 only.
func (*MemoryIndex) ClearEmbeddings ¶ added in v0.12.0
func (idx *MemoryIndex) ClearEmbeddings()
ClearEmbeddings resets all embeddings to NULL and clears the embedding cache. Called when the embedding model changes (dimensions may differ). Errors are logged rather than returned because callers are best-effort (an HTTP handler invalidating state on config change), but silent failure would leave the index in an inconsistent state.
func (*MemoryIndex) IndexFiles ¶
func (idx *MemoryIndex) IndexFiles(agentID string) error
IndexFiles indexes MEMORY.md and daily notes.
func (*MemoryIndex) IndexFilesIfStale ¶
func (idx *MemoryIndex) IndexFilesIfStale(agentID string)
IndexFilesIfStale re-indexes files only if they've changed since last index.
func (*MemoryIndex) IndexMessages ¶
func (idx *MemoryIndex) IndexMessages(agentID string) error
IndexMessages indexes the agent's full transcript by reading every row from agent_messages via loadMessages and rebuilding the message portion of memory_fts/chunks from scratch.
func (*MemoryIndex) IndexNewMessages ¶
func (idx *MemoryIndex) IndexNewMessages(agentID string)
IndexNewMessages incrementally indexes only new messages since last index. Fast-path uses (count, max(updated_at)) as the change cursor: count alone would miss edit-in-place (regenerate, content fix), and max(updated_at) alone would miss a delete that brings the value back to a prior maximum. Together they detect every observable transcript change without an O(rows) scan.
func (*MemoryIndex) Reindex ¶
func (idx *MemoryIndex) Reindex(agentID string) error
Reindex re-indexes all sources for an agent (full rebuild).
func (*MemoryIndex) Search ¶
func (idx *MemoryIndex) Search(query string, limit int) ([]SearchResult, error)
Search performs a FTS5 search and returns relevant context snippets.
type MemoryRecord ¶ added in v0.101.0
type MemoryRecord struct {
AgentID string
Body string
ETag string
UpdatedAt int64
DeletedAt *int64
}
MemoryRecord is the public representation of an agent's MEMORY.md for HTTP / Web UI consumers. Mirrors the shape of store.AgentMemoryRecord without exposing the store package directly so handlers don't need to import internal/store for a single field set.
type Message ¶
type Message struct {
ID string `json:"id"`
Role string `json:"role"` // "user", "assistant", "system"
Content string `json:"content"`
Thinking string `json:"thinking,omitempty"` // intermediate reasoning (shown collapsed in UI)
ToolUses []ToolUse `json:"toolUses,omitempty"`
Attachments []MessageAttachment `json:"attachments,omitempty"`
Timestamp string `json:"timestamp"` // RFC3339
// CreatedAtMillis is the row's created_at in epoch-millis. Not
// serialized (the wire carries the RFC3339 Timestamp) but retained
// in-process so list-ordering can break same-second ties that the
// seconds-resolution RFC3339 string cannot. Zero when the message
// did not originate from a store record.
CreatedAtMillis int64 `json:"-"`
Usage *Usage `json:"usage,omitempty"`
// ETag is the inner identifier of the row's strong HTTP entity
// tag — UNQUOTED (e.g. "v3-deadbeef") so the JSON looks natural;
// the matching HTTP ETag header carries the same identifier
// wrapped in double quotes per RFC 7232 (`"v3-deadbeef"`). The
// Web UI's httpClient.patchWithIfMatch quotes this raw value
// before putting it on the wire as If-Match.
//
// Surfaced inside the JSON so a list fetch
// (GET /api/v1/agents/{id}/messages) gives the Web UI per-message
// etags without a per-row HEAD; PATCH /messages/{msgId} then
// sends this value as If-Match for optimistic concurrency.
// Empty when the message originated outside the v1 store (legacy
// in-memory paths, transitional rows).
ETag string `json:"etag,omitempty"`
}
Message represents a single chat message in the transcript.
type MessageAttachment ¶ added in v0.7.0
type MessageAttachment struct {
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Mime string `json:"mime"`
}
MessageAttachment represents a file attached to a chat message.
type MessagePreview ¶
type MessagePreview struct {
Content string `json:"content"`
Role string `json:"role"`
Timestamp string `json:"timestamp"`
}
MessagePreview is a short summary for agent list display.
type MissingExpectedIDError ¶ added in v0.111.0
type MissingExpectedIDError struct {
Latest string
}
MissingExpectedIDError is returned by PostMessageStrict when the caller omitted expectedLatestMessageId on a room that already has a head. Latest carries the current head so the caller can recover in one step.
func (*MissingExpectedIDError) Error ¶ added in v0.111.0
func (e *MissingExpectedIDError) Error() string
type NotifyMode ¶ added in v0.15.0
type NotifyMode string
NotifyMode controls how a specific member receives group-DM notifications.
- "realtime" (default): notify as soon as the group-level cooldown allows.
- "digest": collect messages for up to DigestWindow seconds (or the group cooldown, whichever is larger) before delivering a single batched turn.
- "muted": do not notify this member at all. The member can still read messages via the API on their own initiative.
const ( NotifyRealtime NotifyMode = "realtime" NotifyDigest NotifyMode = "digest" NotifyMuted NotifyMode = "muted" )
type OTPEntry ¶
type OTPEntry struct {
Label string `json:"label"`
Issuer string `json:"issuer"`
Username string `json:"username"`
Secret string `json:"totpSecret"`
Algorithm string `json:"algorithm,omitempty"`
Digits int `json:"digits,omitempty"`
Period int `json:"period,omitempty"`
}
OTPEntry represents a parsed OTP entry from a QR code or migration payload.
func DecodeQRImage ¶
DecodeQRImage decodes a QR code from an image reader and returns parsed OTP entries.
func ParseOTPURI ¶
ParseOTPURI parses an otpauth:// or otpauth-migration:// URI and returns OTP entries.
type OneShotOpts ¶ added in v0.20.0
type OneShotOpts struct {
// SessionKey, when non-empty, opts INTO Claude session resumption keyed
// by this string instead of staying purely ephemeral. Slack sets this
// to a stable hash of (agentID, channel, threadTS) so repeated DMs in
// the same thread land on the same JSONL — preserving Claude's context
// across turns within that thread, isolated from the agent's WebUI
// session and from other Slack threads. Empty string preserves the
// pre-PR-#12 "fresh ephemeral session per call" behaviour.
//
// Honored only by backends in backendSupportsSessionKey (claude/codex). For
// other backends the manager drops the key and falls back to OneShot,
// rather than risk silently mixing thread contexts on a backend that
// would interpret !OneShot as "resume the agent's latest session".
SessionKey string
// SystemPromptExtra is appended to the system prompt for this call
// only. Slack uses it to inject per-channel/thread context
// ("You are participating in channel #foo, thread …") at a stable
// offset AFTER the cacheable shared prefix, so adding the block does
// not invalidate the prompt cache for other one-shot callers.
SystemPromptExtra string
}
OneShotOpts configures a ChatOneShot invocation. All fields are optional; pass OneShotOpts{} for the legacy ephemeral-session behaviour.
type PersonaRecord ¶ added in v0.101.0
type PersonaRecord struct {
AgentID string
Body string
ETag string
UpdatedAt int64
DeletedAt *int64
}
PersonaRecord is the public representation of an agent's persona for HTTP / Web UI consumers. Mirrors store.AgentPersonaRecord without leaking the store package across the API boundary.
type RateLimitInfo ¶ added in v0.114.0
type RateLimitInfo struct {
Status string `json:"status"`
RateLimitType string `json:"rateLimitType,omitempty"`
ResetsAt int64 `json:"resetsAt,omitempty"`
Utilization float64 `json:"utilization"`
IsUsingOverage bool `json:"isUsingOverage,omitempty"`
SurpassedThreshold float64 `json:"surpassedThreshold,omitempty"`
}
RateLimitInfo mirrors the "rate_limit_info" payload of the Claude CLI's stream-json "rate_limit_event". The CLI emits it mid-turn (observed on 2.1.201) whenever usage crosses a reporting threshold.
status allowed | allowed_warning | rejected rateLimitType seven_day | five_hour (window the utilization is measured over) resetsAt unix seconds at which the window resets utilization 0..1 fraction of the window consumed
type RateLimitSnapshot ¶ added in v0.114.0
type RateLimitSnapshot struct {
RateLimitInfo
ObservedAt int64 `json:"observedAt"` // unix seconds
}
RateLimitSnapshot is a RateLimitInfo stamped with the wall-clock time it was observed, so a stale snapshot loaded after a restart can be aged out or displayed with an "as of" note.
type SearchResult ¶
type SearchResult struct {
Source string `json:"source"` // "message", "memory", "daily"
Snippet string `json:"snippet"` // text snippet with context
ContentHash string `json:"contentHash"` // for dedup/merge across search methods
Timestamp string `json:"timestamp"`
Score float64 `json:"score"`
}
SearchResult represents a single search hit.
type SkippedSessionFile ¶ added in v0.111.0
type SkippedSessionFile struct {
// Path is the file name / relative path inside the session
// store (e.g. "b7d2….jsonl", "terminal/call-1.log").
Path string `json:"path"`
// Reason is a stable, short slug: "oversized", "unreadable",
// "invalid_ref_name", "unreadable_ref", "rollout_path_unknown",
// "rollout_path_invalid", "rollout_missing".
Reason string `json:"reason"`
// SizeBytes is the on-disk size when known (0 otherwise).
SizeBytes int64 `json:"sizeBytes,omitempty"`
}
SkippedSessionFile records one session file the §3.7 device-switch transfer left behind (oversized, unreadable ref, missing rollout, …). Previously a bare filename in a warn log; the structured shape rides the agent-sync wire and the switch response so the loss is visible to the agent (arrival prompt), the operator (switch API response), and the owner (UI notice).
JSON tags are camelCase so the same struct can be stored verbatim under the agent's settings_json (lastTransferSkips) and consumed by the web UI without a mapping layer.
type SlackBotConfig ¶ added in v0.10.0
type SlackBotConfig struct {
Enabled bool `json:"enabled"`
ThreadReplies bool `json:"threadReplies"` // always reply in-thread (default true)
// Reaction patterns — which message types the bot responds to.
// All default to true for backwards compatibility.
RespondDM *bool `json:"respondDM,omitempty"` // respond to direct messages
RespondMention *bool `json:"respondMention,omitempty"` // respond to @mentions in channels
RespondThread *bool `json:"respondThread,omitempty"` // auto-reply in threads with history
}
SlackBotConfig holds Slack bot configuration for an agent.
func (*SlackBotConfig) ReactDM ¶ added in v0.10.0
func (c *SlackBotConfig) ReactDM() bool
ReactDM returns whether the bot should respond to direct messages.
func (*SlackBotConfig) ReactMention ¶ added in v0.10.0
func (c *SlackBotConfig) ReactMention() bool
ReactMention returns whether the bot should respond to @mentions.
func (*SlackBotConfig) ReactThread ¶ added in v0.10.0
func (c *SlackBotConfig) ReactThread() bool
ReactThread returns whether the bot should auto-reply in threads with history.
type StaleExpectedIDError ¶ added in v0.15.0
type StaleExpectedIDError struct {
// Latest is the current head message ID of the group ("" if the group
// has no messages — practically only relevant when a stale cursor
// references a deleted-and-recreated group, which the system does not
// currently allow but the field must still be representable).
Latest string
// NewMessages is the diff of messages strictly newer than the caller's
// expectedLatestMessageId, in chronological order, capped at
// MaxConflictDiff entries (newest-kept).
NewMessages []*GroupMessage
// HasMore is true when older diff entries had to be dropped to fit the
// MaxConflictDiff cap, or when the caller's cursor was not found in the
// transcript at all (so the diff returned is "best effort latest" rather
// than a true delta).
HasMore bool
}
StaleExpectedIDError is returned by PostMessage when the caller's expectedLatestMessageId does not match the current group head. It carries the up-to-date head plus the diff of messages that arrived between the caller's cursor and the head, so the HTTP layer (or any other caller) can respond with a self-contained "you missed these" payload without forcing a second round-trip.
func (*StaleExpectedIDError) Error ¶ added in v0.15.0
func (e *StaleExpectedIDError) Error() string
type SteerFunc ¶ added in v0.112.0
SteerFunc injects an additional user message into an in-flight turn. Returns an error if the turn has already finished (process exited / stdin closed).
type TOTPParams ¶
type TOTPParams struct {
Secret string
Algorithm string // SHA1, SHA256, SHA512
Digits int // 6 or 8
Period int // seconds
}
TOTPParams holds TOTP configuration for a credential.
type TTSConfig ¶ added in v0.19.0
type TTSConfig struct {
// Enabled gates the TTS feature for this agent. Even when the per-
// browser auto-play toggle is on, a disabled agent never synthesizes.
Enabled bool `json:"enabled"`
// Provider selects the synthesis backend: "gemini" (default when empty)
// or "grok" (xAI). Grok ignores Model/StylePrompt.
Provider string `json:"provider,omitempty"`
// Model is the Gemini TTS model id (e.g. "gemini-3.1-flash-tts-preview").
// Empty = use the kojo default. Unused when Provider is "grok".
Model string `json:"model,omitempty"`
// Voice picks a prebuilt voice. For gemini it must be one of the 30
// Gemini voices; for grok it is an xAI voice_id. Empty = provider default.
Voice string `json:"voice,omitempty"`
// StylePrompt is a free-form natural-language instruction prepended to
// the input ("落ち着いた日本語で淡々と…"). Empty = kojo default. Gemini
// only — grok has no free-text style field and ignores this.
StylePrompt string `json:"stylePrompt,omitempty"`
}
TTSConfig holds per-agent text-to-speech configuration.
Validation lives in (*Manager).Update — the model and voice are checked against fixed allow-lists from internal/tts before being stored, so a hand-edited agents.json with garbage values can't reach the synthesize path.
type Task ¶ added in v0.9.0
type Task struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
// ETag is the strong HTTP entity tag of the v1 store row backing
// this task. Web clients echo it via If-Match on PATCH/DELETE for
// optimistic concurrency. Empty when the row pre-dates the cutover
// (defensive — every cutover-era row has one).
ETag string `json:"etag,omitempty"`
}
Task is the API-surface representation of a persistent todo item.
Status uses the v0 vocabulary ("open" / "done") because the Web UI and MCP tool surface have shipped against that for the lifetime of the project. Internally the row lives in agent_tasks with the v1 status vocabulary ('pending' | 'in_progress' | 'done' | 'cancelled'); translation happens at the boundary inside this file via statusToV0 (output) and statusFromV0 (input). v1 statuses that have no v0 equivalent are folded — see statusToV0's docstring for the exact policy — so a hand-edited DB row never leaks an unknown status to a client coded against the two-state model.
type TaskCreateParams ¶ added in v0.9.0
type TaskCreateParams struct {
Title string `json:"title"`
}
TaskCreateParams is the request body for creating a task.
type TaskUpdateParams ¶ added in v0.9.0
TaskUpdateParams is the request body for updating a task. Status accepts only the v0 vocabulary ("open" / "done") to match the existing API contract.
type ThreadLiveSnapshot ¶ added in v0.112.0
type ThreadLiveSnapshot struct {
Status string `json:"status,omitempty"`
Thinking string `json:"thinking,omitempty"`
Text string `json:"text,omitempty"`
ToolUses []ToolUse `json:"toolUses,omitempty"`
Model string `json:"model,omitempty"`
Effort string `json:"effort,omitempty"`
}
ThreadLiveSnapshot is a defensive copy of a thread room's live turn state, safe to hand to callers outside the manager (no shared slices/pointers with the internal registry).
type ToolUse ¶
type ToolUse struct {
ID string `json:"id,omitempty"` // tool call ID for matching results
Name string `json:"name"`
Input string `json:"input"` // JSON string
Output string `json:"output"` // truncated output
// Text holds a subagent narrative snippet when this entry is a
// synthetic "text bubble" child rather than a real tool call (Name
// and Input are empty in that case). Only ever populated on entries
// that live inside Children.
Text string `json:"text,omitempty"`
// Children holds tool calls (and narrative text bubbles) emitted by
// a subagent spawned via the Task tool, when this ToolUse is that
// Task invocation. Populated flat (one level) even for nested
// sub-subagents — see parseClaudeStream's subagentOwner mapping,
// which folds any deeper nesting onto the top-level Task ToolUse
// instead of building a recursive tree.
Children []ToolUse `json:"children,omitempty"`
}
ToolUse represents a single tool invocation within a message.
type TruncateMemoryResult ¶ added in v0.20.0
type TruncateMemoryResult struct {
// Since is the threshold instant, formatted as RFC3339. Entries whose
// timestamp is at or after this are considered "after T" and removed.
Since string `json:"since"`
// MessagesRemoved is the number of kojo transcript records tombstoned
// in the agent_messages table.
MessagesRemoved int `json:"messagesRemoved"`
// ClaudeSessionEntriesRemoved counts JSONL records dropped from
// ~/.claude/projects/<encoded(agentDir)>/<sessionID>.jsonl across every
// session file we touched. Includes records dropped during the
// turn-boundary trim that runs after the timestamp filter.
ClaudeSessionEntriesRemoved int `json:"claudeSessionEntriesRemoved"`
// ClaudeSessionFilesRemoved counts session files we deleted because
// every record in them was at or after the threshold (and so the file
// would have been left empty).
ClaudeSessionFilesRemoved int `json:"claudeSessionFilesRemoved"`
// GrokSessionsRemoved counts UUID-named grok session subdirectories
// dropped from $GROK_HOME/sessions/<encoded(agentDir)>/. Grok's
// events.jsonl / chat_history.jsonl have no kojo-compatible per-record
// timestamp scheme, so any truncate that lands inside a session
// drops the entire session rather than risk replaying a torn turn
// log on the next `grok --resume <id>`. The session_id pointer file
// (<agentDir>/.grok/session_id) is removed alongside so the next
// non-OneShot turn opens a fresh session.
GrokSessionsRemoved int `json:"grokSessionsRemoved"`
// GrokSessionFilesRemoved counts every regular file deleted from
// the dropped grok session subtrees (events.jsonl, chat_history.jsonl,
// summary.json, system_prompt.txt, terminal/*.log, …).
GrokSessionFilesRemoved int `json:"grokSessionFilesRemoved"`
// CodexThreadsRemoved counts Codex app-server threads whose Kojo
// per-agent refs were dropped. Codex rollouts do not carry a
// kojo-compatible per-record timestamp, so truncation drops the
// whole referenced thread and lets the next turn start fresh.
CodexThreadsRemoved int `json:"codexThreadsRemoved"`
// CodexSessionFilesRemoved counts rollout JSONL files removed while
// dropping the referenced Codex threads.
CodexSessionFilesRemoved int `json:"codexSessionFilesRemoved"`
// DiaryFilesRemoved counts memory/YYYY-MM-DD.md files we deleted
// outright because their date is strictly after the threshold's date
// (in JST — diary timestamps are JST-local). Also counts memory_entries
// rows whose name matched a removed file.
DiaryFilesRemoved int `json:"diaryFilesRemoved"`
// DiaryEntriesRemoved counts `- HH:MM — ...` bullet lines we removed
// from memory/YYYY-MM-DD.md files matching the threshold's date (or
// from their memory_entries body when the row outlives the file).
DiaryEntriesRemoved int `json:"diaryEntriesRemoved"`
}
TruncateMemoryResult summarises what TruncateMemoryAt removed. Counts are best-effort; entries we couldn't parse are kept verbatim and not counted (same forgiving stance as ResetData — never lose a malformed line just because its timestamp didn't parse).
type Usage ¶
type Usage struct {
InputTokens int `json:"inputTokens"`
OutputTokens int `json:"outputTokens"`
CacheReadInputTokens int `json:"cacheReadInputTokens,omitempty"`
CacheCreationInputTokens int `json:"cacheCreationInputTokens,omitempty"`
// CostUSD is the backend-reported total cost for the invocation (from
// the Claude CLI "result" event's total_cost_usd), covering subagent
// usage and per-model rates. Zero when the backend didn't report a
// cost; the UI then falls back to a client-side estimate.
CostUSD float64 `json:"costUSD,omitempty"`
}
Usage tracks token consumption for a message.
CacheReadInputTokens / CacheCreationInputTokens are surfaced from Claude's stream so we can diagnose prompt-cache hit/miss patterns. A high CacheCreation:CacheRead ratio across consecutive turns indicates the system prompt or message prefix is changing between turns and breaking the cache — which directly inflates input cost and adds per-turn latency. (Note: cache state itself does not change the model's logical context length; what does grow is the volume of input tokens billed.)
Source Files
¶
- agent.go
- attach_open_unix.go
- attach_scan.go
- attach_skill.go
- auto_effort.go
- autosummary.go
- avatar.go
- backend.go
- backend_claude.go
- backend_codex.go
- backend_custom.go
- backend_grok.go
- backend_llamacpp.go
- blob_hydrate.go
- broadcaster.go
- chat_accumulator.go
- claude_session.go
- claude_session_transfer.go
- codex_session_ref.go
- codex_session_transfer.go
- codex_steer.go
- credential.go
- cron.go
- cron_expr.go
- cron_lock_kv.go
- device_switch_skill.go
- embedding.go
- errors.go
- file_watcher.go
- generate.go
- grok_session_transfer.go
- groupdm.go
- groupdm_manager.go
- groupdm_mentions.go
- guides.go
- holiday_jp.go
- hub_local_update.go
- id.go
- jsonl_scanner.go
- keyedmutex.go
- manager.go
- manager_busy.go
- manager_fork.go
- manager_lifecycle.go
- manager_memory.go
- mcp_config.go
- memory.go
- memory_entries_io.go
- memory_index.go
- memory_io.go
- memory_sync.go
- message.go
- persona_io.go
- persona_sync.go
- ratelimit.go
- recent_context.go
- restart_wake.go
- store.go
- subagent_activity.go
- subagent_tail.go
- task.go
- token_store.go
- totp.go
- transcript.go
- transfer_skips.go
- truncate_memory.go
- workspace_sync.go