daemon

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Mar 15, 2026 License: MIT Imports: 48 Imported by: 0

Documentation

Overview

Package daemon provides background sync operations for the ledger.

Package daemon provides heartbeat file management for daemon health monitoring.

Heartbeat Storage Strategy

Heartbeats are stored in different locations depending on what they're monitoring:

  1. WORKSPACE heartbeats: ~/.cache/sageox/<endpoint>/heartbeats/<workspace_id>.jsonl
  2. LEDGER heartbeats: ~/.cache/sageox/<endpoint>/heartbeats/<workspace_id>_ledger.jsonl
  3. TEAM heartbeats: ~/.cache/sageox/<endpoint>/heartbeats/<team_id>.jsonl

Why workspace_id (not repo_id) for workspace/ledger heartbeats?

CRITICAL: workspace_id is a hash of the project root PATH, not the repo identity. This prevents collisions when users have multiple git worktrees of the same repo.

Scenario that breaks with repo_id:

  • User has repo "foo" with repo_id="repo_abc123"
  • User creates worktrees: ~/work/foo-main, ~/work/foo-feature1, ~/work/foo-feature2
  • Each worktree has its own daemon instance (one per project root)
  • All three daemons would write to the SAME heartbeat file: repo_abc123.jsonl
  • Result: Race conditions, confusing PID/workspace data, can't track per-worktree health

By using workspace_id (hash of ~/work/foo-main vs ~/work/foo-feature1):

  • Each worktree gets its own heartbeat file: a1b2c3d4.jsonl, e5f6g7h8.jsonl, etc.
  • Each daemon's heartbeat is isolated
  • Can track health of each worktree independently

Why global cache (not in-repo .sageox/cache/)?

Originally heartbeats were written to .sageox/cache/heartbeats.jsonl inside each repo. This worked fine for workspaces (each worktree has its own .sageox/), but caused problems for ledgers and team contexts:

  • Ledgers are SHARED git repos (in sibling dirs like project_sageox/)
  • Team contexts are SHARED git repos (in ~/.local/share/sageox/<endpoint>/teams/)
  • Writing .sageox/ directories into shared repos pollutes them with machine-specific data
  • Even with .gitignore, the directories shouldn't exist in shared repos

Solution: Global cache for all heartbeats, using appropriate identifiers:

  • workspace_id: Unique per daemon instance (solves worktree problem)
  • team_id: Shared across workspaces (team contexts are multi-project)

Package daemon implements the background sync daemon for ledger and team contexts.

The daemon performs git pull (read) operations for ledger and team context sync. The CLI handles add/commit/push (write) operations via the session upload pipeline. Exception: GitHubSyncManager also performs add/commit/push for data/github/ files, since these are idempotent and last-write-wins safe (accept-theirs conflict resolution).

NETWORK DISCONNECTION HANDLING

The daemon operates normally when the internet is disconnected. This is NOT a failure mode - developers frequently work offline (planes, cafes, etc.).

Design principles:

  • Network failures are expected and handled gracefully
  • Logs should NOT fill up during disconnection (use Warn, not Error)
  • Operations retry on the next sync interval when connectivity returns
  • The daemon should return to normal operation automatically when reconnected

SageOx is multiplayer, but the underlying git repos work fine offline. Only API calls and git fetch require daemon connectivity; push is CLI-side.

Index

Constants

View Source
const (
	ErrorCodeSyncFailed     = "sync_failed"
	ErrorCodeAuthExpired    = "auth_expired"
	ErrorCodeGitConflict    = "git_conflict"
	ErrorCodeNetworkError   = "network_error"
	ErrorCodeDiskFull       = "disk_full"
	ErrorCodePermissionDeny = "permission_denied"
)

Error codes for common daemon errors.

View Source
const (
	StatusActive = "active" // recently received heartbeat
	StatusIdle   = "idle"   // no heartbeat within idle threshold
	StatusStale  = "stale"  // no heartbeat within stale threshold
	StatusExited = "exited" // parent process confirmed dead via kill(pid, 0)
)

Instance status constants.

View Source
const (
	// IdleThreshold is how long without heartbeat before instance is "idle".
	// Agents typically heartbeat every ~5s, so 30s = missed ~6 heartbeats.
	IdleThreshold = 30 * time.Second

	// StaleThreshold is how long without heartbeat before instance is "stale".
	// Stale instances are candidates for cleanup.
	StaleThreshold = 5 * time.Minute

	// MaxAge is the maximum age of an instance before auto-cleanup.
	// Prevents abandoned instances from accumulating indefinitely.
	MaxAge = 24 * time.Hour

	// CleanupInterval is how often to run the cleanup routine.
	CleanupInterval = 1 * time.Minute

	// MaxInstances caps the number of tracked instances to prevent unbounded growth.
	// When exceeded, oldest stale instances are evicted first.
	MaxInstances = 100
)

Default instance timing thresholds.

View Source
const (
	MsgTypeStatus          = "status"
	MsgTypeSync            = "sync"
	MsgTypeTeamSync        = "team_sync" // on-demand team context sync
	MsgTypePing            = "ping"
	MsgTypeStop            = "stop"
	MsgTypeVersion         = "version"
	MsgTypeSyncHistory     = "sync_history"
	MsgTypeHeartbeat       = "heartbeat"        // one-way, no response expected
	MsgTypeCheckout        = "checkout"         // synchronous git clone operation
	MsgTypeTelemetry       = "telemetry"        // one-way, no response expected
	MsgTypeFriction        = "friction"         // one-way, friction event for analytics
	MsgTypeGetErrors       = "get_errors"       // retrieve unviewed daemon errors
	MsgTypeMarkErrors      = "mark_errors"      // mark errors as viewed
	MsgTypeSessions        = "sessions"         // get active agent sessions (deprecated: use instances)
	MsgTypeInstances       = "instances"        // get active agent instances
	MsgTypeDoctor          = "doctor"           // trigger daemon health checks (anti-entropy, etc.)
	MsgTypeTriggerGC       = "trigger_gc"       // force GC reclone for team contexts
	MsgTypeCodeIndex       = "code_index"       // index local code with progress
	MsgTypeCodeStatus      = "code_status"      // get code index status/stats
	MsgTypeNotifications   = "notifications"    // query pending team context change notifications
	MsgTypeSessionFinalize = "session_finalize" // one-way, trigger async session upload+finalization
)

Message types for IPC communication.

View Source
const (
	SeverityWarning  = "warning"
	SeverityError    = "error"
	SeverityCritical = "critical"
)

Severity constants for DaemonIssue. No "info" level - if the daemon needs help, it's at least a warning.

View Source
const (
	IssueTypeMergeConflict      = "merge_conflict"
	IssueTypeMissingScaffolding = "missing_scaffolding"
	IssueTypeDiverged           = "diverged"
	IssueTypeAuthExpiring       = "auth_expiring"
	IssueTypeGitLock            = "git_lock"
	IssueTypeCloneFailed        = "clone_failed"
	IssueTypeSyncBackoff        = "sync_backoff"
	IssueTypeDirtyWorkspace     = "dirty_workspace"
)

Issue type constants.

View Source
const (
	// DefaultStalenessThreshold is the duration after which sync data is considered stale.
	DefaultStalenessThreshold = 24 * time.Hour
)
View Source
const SessionStaleThreshold = StaleThreshold

Backward compatibility aliases for SessionStaleThreshold TODO: Remove after updating all consumers

Variables

View Source
var ErrCloneSemaphoreTimeout = errors.New("clone semaphore timeout")

ErrCloneSemaphoreTimeout indicates all clone slots were busy and the wait timed out. This is a transient error that should be retried on the next sync cycle without exponential backoff — the slots will free up when in-progress clones finish.

View Source
var ErrInvalidRepoPath = errors.New("invalid repo path: path traversal or unsafe location detected")

ErrInvalidRepoPath indicates the repo path failed security validation.

View Source
var ErrNotRunning = errors.New("daemon not running")

ErrNotRunning indicates the daemon is not running.

View Source
var ErrShutdownTimeout = errors.New("shutdown timeout: goroutines did not finish in time")

ErrShutdownTimeout indicates goroutines did not finish within the timeout.

Functions

func CurrentWorkspaceID

func CurrentWorkspaceID() string

CurrentWorkspaceID returns the ID for the current working directory. Prefers repo_id-based identity so multiple clones/worktrees of the same repo share one daemon. Falls back to path-based ID for non-initialized repos. The result is cached on first call so the daemon continues to use the correct workspace ID even if its CWD is later deleted (e.g. macOS tmpdir cleanup while the daemon is running long-term).

func EnsureDaemon

func EnsureDaemon() error

EnsureDaemon ensures the daemon is running, starting it if necessary. Claude manages the daemon process lifecycle (launching and killing), so setsid/detach is no longer needed. The daemon relies on its inactivity timeout to self-exit when no heartbeats arrive. Returns nil on success (daemon is running), or an error if it couldn't be started. This is a no-op if daemon is already running or disabled via SAGEOX_DAEMON=false.

The function waits up to 2 seconds for the daemon to become available after starting.

func EnsureDaemonAttached

func EnsureDaemonAttached() error

EnsureDaemonAttached is an alias for EnsureDaemon. Previously started the daemon without setsid (attached to caller's process group). Now that Claude manages the daemon lifecycle, setsid is removed entirely and both functions behave identically.

func ErrorStorePath

func ErrorStorePath() string

ErrorStorePath returns the default path to the error store file.

func ErrorStorePathForWorkspace

func ErrorStorePathForWorkspace(workspaceID string) string

ErrorStorePathForWorkspace returns the error store path for a specific workspace.

func FormatDaemonList

func FormatDaemonList(daemons []DaemonInfo) string

FormatDaemonList formats a list of daemons for display.

func FormatNotRunning added in v0.3.0

func FormatNotRunning(inProject bool) string

FormatNotRunning renders the "daemon not running" state with context. inProject indicates whether the user is inside an initialized SageOx project.

func FormatStarting added in v0.3.0

func FormatStarting() string

FormatStarting renders the daemon status when the process exists but IPC isn't ready yet.

func FormatStatus

func FormatStatus(status *StatusData, cliVersion string) string

FormatStatus renders compact daemon status (Tufte-inspired: maximize data-ink ratio). cliVersion is the current CLI version for match comparison.

func FormatStatusVerbose

func FormatStatusVerbose(status *StatusData, history []SyncEvent, cliVersion string) string

FormatStatusVerbose includes sparkline, internals, and sync history table.

func FormatStatusWithSparkline added in v0.3.0

func FormatStatusWithSparkline(status *StatusData, history []SyncEvent, cliVersion string) string

FormatStatusWithSparkline adds a 4h activity sparkline to the status output.

func HasConfirmRequired

func HasConfirmRequired(issues []DaemonIssue) bool

HasConfirmRequired returns true if any issue in the slice requires confirmation.

func IsDaemonDisabled

func IsDaemonDisabled() bool

IsDaemonDisabled returns true if the daemon has been explicitly disabled via the SAGEOX_DAEMON=false environment variable.

func IsHealthy

func IsHealthy() error

IsHealthy checks if the daemon is running AND responsive. Returns nil if healthy, error describing the failure mode otherwise.

Uses a 100ms timeout - plenty for localhost IPC. If you need custom timeouts, use NewClientWithTimeout(t).Ping() directly.

func IsRunning

func IsRunning() bool

IsRunning checks if a daemon is currently running and responsive. Uses socket-based ping detection. Claude manages the daemon process lifecycle, so flock-based locking is no longer needed.

func IsStarting added in v0.3.0

func IsStarting() bool

IsStarting checks if a daemon process exists (PID file with live process) but is not yet responding to IPC. This happens during startup throttling or initial setup before the IPC socket is ready.

func LegacyWorkspaceID added in v0.5.0

func LegacyWorkspaceID() string

LegacyWorkspaceID returns the old path-based workspace ID for the current working directory. Needed for migration: stopping daemons that were started under the old path-hash scheme. Cached separately from CurrentWorkspaceID to avoid interference.

func LogPath

func LogPath() string

LogPath returns the path to the daemon log file for the current workspace. Requires project to be initialized with repo_id.

func LogPathForWorkspace

func LogPathForWorkspace(repoID, workspaceID string) string

LogPathForWorkspace returns the log path for a specific workspace and repo.

func MaxIssueSeverity

func MaxIssueSeverity(issues []DaemonIssue) string

MaxIssueSeverity returns the highest severity among the given issues. Returns empty string if the slice is empty.

func PidPath

func PidPath() string

PidPath returns the path to the daemon PID file for the current workspace. Note: PID files are NOT used for liveness detection - use file locks instead.

func PidPathForWorkspace

func PidPathForWorkspace(workspaceID string) string

PidPathForWorkspace returns the PID path for a specific workspace.

func RegisterDaemon

func RegisterDaemon(workspacePath, version string) error

RegisterDaemon registers the current daemon in the registry.

func RegistryPath

func RegistryPath() string

RegistryPath returns the path to the daemon registry file.

func RepoBasedWorkspaceID added in v0.5.0

func RepoBasedWorkspaceID(projectRoot string) string

RepoBasedWorkspaceID returns a workspace ID derived from repo_id in .sageox/config.json. Multiple clones or worktrees of the same repo produce the same ID, so they share a single daemon. Falls back to path-based WorkspaceID if repo_id is unavailable.

func SaveSyncState added in v0.3.0

func SaveSyncState(workspacePath string, state *SyncState) error

SaveSyncState writes sync state to .sageox/cache/sync-state.json within workspacePath. Writes to .sageox/cache/ which is gitignored — local-only, never committed to the ledger.

func ShouldUseDaemon

func ShouldUseDaemon() bool

ShouldUseDaemon returns true if we should attempt to use the daemon. Returns true if the daemon is currently running.

func SocketPath

func SocketPath() string

SocketPath returns the path to the daemon Unix socket for the current workspace.

func SocketPathForWorkspace

func SocketPathForWorkspace(workspaceID string) string

SocketPathForWorkspace returns the socket path for a specific workspace.

func StabilizeCWD added in v0.3.0

func StabilizeCWD()

StabilizeCWD moves the daemon's working directory to $HOME so that git commands don't fail if the original CWD is deleted (e.g. tmpdir cleanup). Must be called AFTER CurrentWorkspaceID() has cached the workspace ID.

func UnregisterDaemon

func UnregisterDaemon() error

UnregisterDaemon removes the current daemon from the registry. Uses cached workspace ID since CWD may have been stabilized to $HOME.

func UserHeartbeatPath

func UserHeartbeatPath(ep, repoID, workspaceID string) string

UserHeartbeatPath returns the global cache path for workspace heartbeat.

CRITICAL: Uses BOTH repo_id AND workspace_id in filename.

Why both?

  • workspace_id (hash of path) prevents collisions between worktrees
  • repo_id makes debugging easier - you can see which repo the heartbeat belongs to

Format: ~/.cache/sageox/<endpoint>/heartbeats/<repo_id>_<workspace_id>.jsonl

Example with multiple worktrees of repo "foo" (repo_id=repo_abc123):

  • Worktree ~/work/foo-main → workspace_id=a1b2c3d4 → repo_abc123_a1b2c3d4.jsonl
  • Worktree ~/work/foo-fix123 → workspace_id=e5f6g7h8 → repo_abc123_e5f6g7h8.jsonl

Both have same repo_id but different workspace_ids → no collision, easy to identify.

func UserLedgerHeartbeatPath

func UserLedgerHeartbeatPath(ep, repoID, workspaceID string) string

UserLedgerHeartbeatPath returns the global cache path for ledger heartbeat.

CRITICAL: Uses BOTH repo_id AND workspace_id because each worktree has its own ledger. Ledgers use the sibling directory pattern: <project>_sageox/<endpoint>/ledger So different worktrees → different ledger paths → need different heartbeat files.

Format: ~/.cache/sageox/<endpoint>/heartbeats/<repo_id>_<workspace_id>_ledger.jsonl

Example:

  • Worktree ~/work/foo-main has ledger ~/work/foo-main_sageox/sageox.ai/ledger → repo_abc123_a1b2c3d4_ledger.jsonl
  • Worktree ~/work/foo-fix has ledger ~/work/foo-fix_sageox/sageox.ai/ledger → repo_abc123_e5f6g7h8_ledger.jsonl

These are DIFFERENT ledger repos → separate heartbeats, but same repo_id for grouping.

func UserTeamHeartbeatPath

func UserTeamHeartbeatPath(ep, teamID string) string

UserTeamHeartbeatPath returns the global cache path for team context heartbeat.

Uses team_id (NOT workspace_id) because team contexts are shared across projects. A team context repo at ~/.local/share/sageox/<endpoint>/teams/<team_id>/ may be used by multiple projects/worktrees simultaneously. All daemons monitoring this team context write to the same heartbeat file (last-write-wins is acceptable for monitoring data - we just care that SOME daemon is syncing it).

~/.cache/sageox/<endpoint>/heartbeats/<team_id>.jsonl

Example:

  • Team "engineering" (team_id=team_abc123) is used by projects A, B, C
  • All three daemons write to the same heartbeat: team_abc123.jsonl
  • Doctor sees "team synced 2m ago" - doesn't matter which daemon did it

func Version

func Version() string

Version returns the daemon version including build timestamp. Used for heartbeat version comparison to detect when CLI has been rebuilt. Includes BuildDate so dirty rebuilds (same git hash) still trigger restart.

func WorkspaceID

func WorkspaceID(workspacePath string) string

WorkspaceID generates a stable identifier for a workspace path. Uses SHA256 of the real (symlink-resolved) absolute path, truncated to 8 chars. This is the legacy path-based ID, still used for non-initialized repos.

func WriteHeartbeatToPath

func WriteHeartbeatToPath(heartbeatPath string, entry HeartbeatEntry) error

WriteHeartbeatToPath writes a heartbeat entry to an explicit path (for global cache). Maintains a rolling history of the last N heartbeats. Used for writing to ~/.cache/sageox/<endpoint>/heartbeats/<id>.jsonl

Types

type ActivityEntry

type ActivityEntry struct {
	Key        string      `json:"key"`
	Count      int         `json:"count"`
	Last       time.Time   `json:"last"`
	Timestamps []time.Time `json:"timestamps,omitempty"` // for sparkline
}

ActivityEntry represents activity for a single key.

type ActivitySummary

type ActivitySummary struct {
	Repos      []ActivityEntry `json:"repos,omitempty"`
	Teams      []ActivityEntry `json:"teams,omitempty"`
	Workspaces []ActivityEntry `json:"workspaces,omitempty"`
	Agents     []ActivityEntry `json:"agents,omitempty"` // connected agent sessions
}

ActivitySummary returns a summary of all activity for status display.

type ActivityTracker

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

ActivityTracker stores recent timestamps for any named key. Thread-safe. Capped to N entries per key for memory efficiency. Also limits total number of keys to prevent unbounded memory growth. Useful for tracking heartbeats, syncs, and other events for sparkline display.

func NewActivityTracker

func NewActivityTracker(capacity int) *ActivityTracker

NewActivityTracker creates a new activity tracker with the given capacity per key.

func NewActivityTrackerWithMaxKeys

func NewActivityTrackerWithMaxKeys(capacity, maxKeys int) *ActivityTracker

NewActivityTrackerWithMaxKeys creates a tracker with custom capacity and key limit.

func (*ActivityTracker) Clear

func (t *ActivityTracker) Clear(key string)

Clear removes all entries for the given key.

func (*ActivityTracker) Count

func (t *ActivityTracker) Count(key string) int

Count returns the number of recorded events for the key.

func (*ActivityTracker) Get

func (t *ActivityTracker) Get(key string) []time.Time

Get returns recent timestamps for the key (oldest first). Returns nil if key not found.

func (*ActivityTracker) Has added in v0.3.0

func (t *ActivityTracker) Has(key string) bool

Has returns true if the key has been recorded at least once.

func (*ActivityTracker) Keys

func (t *ActivityTracker) Keys() []string

Keys returns all tracked keys.

func (*ActivityTracker) Last

func (t *ActivityTracker) Last(key string) time.Time

Last returns the most recent timestamp for the key. Returns zero time if key not found or no entries.

func (*ActivityTracker) Record

func (t *ActivityTracker) Record(key string)

Record adds a timestamp for the given key.

func (*ActivityTracker) RecordAt

func (t *ActivityTracker) RecordAt(key string, ts time.Time)

RecordAt adds a specific timestamp for the given key.

func (*ActivityTracker) Reset

func (t *ActivityTracker) Reset()

Reset removes all tracked data.

type AgentContextStats added in v0.3.0

type AgentContextStats struct {
	ContextTokens int64 `json:"context_tokens"`
	CommandCount  int   `json:"command_count"`
}

AgentContextStats holds cumulative context consumption for an agent.

type AgentSession

type AgentSession struct {
	// AgentID is the short agent identifier (e.g., "Oxa7b3").
	AgentID string `json:"agent_id"`

	// WorkspacePath is the workspace/repo the agent is working in.
	WorkspacePath string `json:"workspace_path"`

	// LastHeartbeat is when the agent last sent a heartbeat.
	LastHeartbeat time.Time `json:"last_heartbeat"`

	// HeartbeatCount is the number of heartbeats received from this agent.
	HeartbeatCount int `json:"heartbeat_count"`

	// Status is "active" (recent heartbeat) or "idle" (stale heartbeat).
	Status string `json:"status"`
}

AgentSession represents an active agent session from a daemon. Used by the sessions IPC message to report connected agents.

func GetAllSessions

func GetAllSessions() ([]AgentSession, error)

GetAllSessions queries all running daemons and aggregates their agent sessions. Returns sessions from all workspaces, sorted by last heartbeat (most recent first). Deprecated: Use GetAllInstances instead.

type AuthenticatedUser

type AuthenticatedUser struct {
	Email string `json:"email,omitempty"`
	ID    string `json:"id,omitempty"`
}

AuthenticatedUser holds info about the authenticated user.

type CallerInfo added in v0.5.0

type CallerInfo struct {
	ID       string    `json:"id"`                 // CallerID (path-based hash)
	Path     string    `json:"path"`               // absolute path of the clone/worktree
	LastSeen time.Time `json:"last_seen"`          // last heartbeat received
	AgentID  string    `json:"agent_id,omitempty"` // last known agent in this clone
}

CallerInfo tracks a connected clone/worktree.

type ChangeEntry added in v0.5.0

type ChangeEntry struct {
	Path      string    `json:"path"`       // relative file path within team context
	ChangedAt time.Time `json:"changed_at"` // when change was detected
	TeamID    string    `json:"team_id"`
	TeamName  string    `json:"team_name"`
}

ChangeEntry tracks a single file change in a team context.

type CheckoutPayload

type CheckoutPayload struct {
	RepoPath string `json:"repo_path"` // target path for clone
	CloneURL string `json:"clone_url"` // git clone URL
	RepoType string `json:"repo_type"` // "ledger" or "team_context"
}

CheckoutPayload is the payload for checkout requests.

type CheckoutProgress

type CheckoutProgress struct {
	Stage   string `json:"stage"`             // "connecting", "cloning", "verifying"
	Percent *int   `json:"percent,omitempty"` // 0-100, nil if unknown
	Message string `json:"message"`           // human-readable progress message
}

CheckoutProgress is sent during long-running checkout operations.

type CheckoutResult

type CheckoutResult struct {
	Path          string `json:"path"`           // actual path where repo exists
	AlreadyExists bool   `json:"already_exists"` // true if repo already existed
	Cloned        bool   `json:"cloned"`         // true if we performed a clone
}

CheckoutResult is the result of a checkout operation.

type Client

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

Client provides IPC communication with the daemon.

func NewClient

func NewClient() *Client

NewClient creates a new IPC client.

func NewClientWithSocket

func NewClientWithSocket(socketPath string) *Client

NewClientWithSocket creates an IPC client for a specific socket path. Used when connecting to daemons for other workspaces.

func NewClientWithTimeout

func NewClientWithTimeout(timeout time.Duration) *Client

NewClientWithTimeout creates an IPC client with custom timeout.

func TryConnect

func TryConnect() *Client

TryConnect attempts to connect to the daemon. Returns the client if connected, nil otherwise.

func TryConnectForCheckout

func TryConnectForCheckout() *Client

TryConnectForCheckout attempts to connect for checkout operations. Uses a long timeout since clones can take time.

func TryConnectForCheckoutWithRetry

func TryConnectForCheckoutWithRetry(maxRetries int, initialDelay time.Duration) *Client

TryConnectForCheckoutWithRetry is like TryConnectWithRetry but uses longer timeouts for checkout operations.

func TryConnectForSync

func TryConnectForSync() *Client

TryConnectForSync attempts to connect for sync operations. Uses a longer timeout since syncs can take time.

func TryConnectForSyncWithRetry

func TryConnectForSyncWithRetry(maxRetries int, initialDelay time.Duration) *Client

TryConnectForSyncWithRetry is like TryConnectWithRetry but uses longer timeouts for sync operations.

func TryConnectOrDirect

func TryConnectOrDirect() *Client

TryConnectOrDirect attempts to connect to the daemon. Returns nil if daemon is not running or unreachable. On transient connection failures, retries once with exponential backoff.

func TryConnectOrDirectForCheckout

func TryConnectOrDirectForCheckout() *Client

TryConnectOrDirectForCheckout is like TryConnectOrDirect but uses longer timeouts for checkout operations.

func TryConnectOrDirectForSync

func TryConnectOrDirectForSync() *Client

TryConnectOrDirectForSync is like TryConnectOrDirect but uses longer timeouts for sync operations.

func TryConnectWithRetry

func TryConnectWithRetry(maxRetries int, initialDelay time.Duration) *Client

TryConnectWithRetry attempts to connect to the daemon with retry logic. On transient failures, retries up to maxRetries times with exponential backoff. initialDelay is the delay before the first retry.

func (*Client) Checkout

func (c *Client) Checkout(payload CheckoutPayload, onProgress ProgressCallback) (*CheckoutResult, error)

Checkout requests the daemon to clone a repository. The onProgress callback is called for each progress update (may be nil). Uses a long timeout (60s) since clones can take time.

func (*Client) CodeIndex added in v0.4.0

func (c *Client) CodeIndex(payload CodeIndexPayload, onProgress ProgressCallback) (*CodeIndexResult, error)

CodeIndex requests the daemon to index code with progress updates. Uses a long timeout (5 minutes) since indexing can take time for large repos.

func (*Client) CodeStatus added in v0.4.0

func (c *Client) CodeStatus() (*CodeDBStats, error)

CodeStatus requests the current code index status from the daemon.

func (*Client) Connect

func (c *Client) Connect() (net.Conn, error)

Connect attempts to connect to the daemon. Returns error if daemon is not running.

func (*Client) Doctor

func (c *Client) Doctor() (*DoctorResponse, error)

Doctor triggers daemon health checks including anti-entropy (self-healing). Returns the results of the health checks.

func (*Client) GetUnviewedErrors

func (c *Client) GetUnviewedErrors() ([]StoredError, error)

GetUnviewedErrors retrieves unviewed daemon errors.

func (*Client) Instances

func (c *Client) Instances() ([]InstanceInfo, error)

Instances gets active agent instances from this daemon.

func (*Client) MarkErrorsViewed

func (c *Client) MarkErrorsViewed(ids []string) error

MarkErrorsViewed marks errors as viewed. If ids is empty, marks all errors as viewed.

func (*Client) Notifications added in v0.5.0

func (c *Client) Notifications(agentID string) (*NotificationsResponse, error)

Notifications queries pending team context change notifications for an agent. Returns changes since the agent last checked. The first call registers the cursor and returns empty; subsequent calls return changes since the previous call.

func (*Client) Ping

func (c *Client) Ping() error

Ping checks if the daemon is responsive.

func (*Client) RequestSync

func (c *Client) RequestSync() error

RequestSync requests the daemon to perform a sync.

func (*Client) SendOneWay

func (c *Client) SendOneWay(msg Message) error

SendOneWay sends a message without waiting for response. Connect, write, close immediately - truly fire-and-forget at IPC layer. Used for heartbeats and other non-blocking notifications.

func (*Client) SessionFinalize added in v0.5.0

func (c *Client) SessionFinalize(payload SessionFinalizeIPCPayload) error

SessionFinalize sends a fire-and-forget request to finalize a session. The daemon will upload to LFS, commit, push, and generate summary artifacts.

func (*Client) Sessions

func (c *Client) Sessions() ([]AgentSession, error)

Sessions gets active agent sessions from this daemon. Deprecated: Use Instances() instead.

func (*Client) Status

func (c *Client) Status() (*StatusData, error)

Status gets the daemon status.

func (*Client) Stop

func (c *Client) Stop() error

Stop requests the daemon to stop.

func (*Client) SyncHistory

func (c *Client) SyncHistory() ([]SyncEvent, error)

SyncHistory gets the recent sync history.

func (*Client) SyncWithProgress

func (c *Client) SyncWithProgress(onProgress ProgressCallback) error

SyncWithProgress requests the daemon to perform a sync with progress updates. The onProgress callback is called for each progress update (may be nil). Uses a 30s timeout since syncs can take time.

func (*Client) TeamSyncWithProgress

func (c *Client) TeamSyncWithProgress(onProgress ProgressCallback) error

TeamSyncWithProgress requests the daemon to sync all team contexts with progress updates. The onProgress callback is called for each progress update (may be nil). Uses a 60s timeout since syncing multiple teams can take time.

func (*Client) TriggerGC added in v0.3.0

func (c *Client) TriggerGC() (*TriggerGCResponse, error)

TriggerGC requests the daemon to force a GC reclone of team contexts.

type CodeDBManager added in v0.4.0

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

CodeDBManager manages CodeDB indexing in the daemon. It ensures only one indexing operation runs at a time and tracks index status.

Concurrency note: The in-process mutex prevents concurrent indexing within a single daemon, but today multiple daemons can exist for the same repo (one per worktree). Cross-process safety currently relies on SQLite WAL mode with busy_timeout(5000ms) — concurrent readers are fine, but two daemons indexing simultaneously could contend on SQLite/Bleve write locks. This is a known short-term limitation. When the daemon model moves to one-per-repo (shared across worktrees), the in-process mutex will be sufficient and no flock will be needed.

func NewCodeDBManager added in v0.4.0

func NewCodeDBManager(projectRoot string, logger *slog.Logger, telemetry *TelemetryCollector) *CodeDBManager

NewCodeDBManager creates a new CodeDB manager for the given project root. Resolves the shared CodeDB path via project config (ledger cache). Falls back to the legacy per-worktree path if project config is unavailable.

func (*CodeDBManager) CheckFreshness added in v0.4.0

func (m *CodeDBManager) CheckFreshness(ctx context.Context)

CheckFreshness checks if the index needs refreshing and triggers a background re-index if needed. If no index exists yet, creates the initial index. This is non-blocking and safe to call from the scheduler or daemon startup.

func (*CodeDBManager) Index added in v0.4.0

Index runs indexing with progress reporting. Only one indexing operation runs at a time. If indexing is already in progress, returns an error immediately.

TODO: When multiple daemons share the same CodeDB (worktree scenario), add a filesystem flock on the data dir to prevent concurrent write contention across processes. Until then, busy_timeout(5000ms) on SQLite provides best-effort protection but Bleve's bolt backend only allows one writer at a time and will error if two daemons index simultaneously.

func (*CodeDBManager) SetLedgerPath added in v0.5.0

func (m *CodeDBManager) SetLedgerPath(path string)

SetLedgerPath sets the ledger checkout path for GitHub data indexing. Called by the daemon when the ledger workspace is discovered.

func (*CodeDBManager) Stats added in v0.4.0

func (m *CodeDBManager) Stats() CodeDBStats

Stats returns current index statistics. Returns cached stats from the last index run to avoid blocking on SQLite during active indexing. Only queries the DB on cold start (no cached stats and not currently indexing).

type CodeDBStats added in v0.4.0

type CodeDBStats struct {
	Commits     int         `json:"commits"`
	Blobs       int         `json:"blobs"`
	Symbols     int         `json:"symbols"`
	Comments    int         `json:"comments"`
	PRs         int         `json:"prs"`
	Issues      int         `json:"issues"`
	Repos       []RepoStats `json:"repos,omitempty"`
	LastIndexed time.Time   `json:"last_indexed,omitempty"`
	IndexingNow bool        `json:"indexing_now"`
	LastError   string      `json:"last_error,omitempty"`
	DataDir     string      `json:"data_dir"`
	IndexExists bool        `json:"index_exists"`
}

CodeDBStats tracks index statistics.

type CodeIndexPayload added in v0.4.0

type CodeIndexPayload struct {
	// URL is an optional remote git URL to index. If empty, indexes the local repo.
	URL string `json:"url,omitempty"`
	// Full wipes the existing index before rebuilding. Used by 'ox index --full'.
	Full bool `json:"full,omitempty"`
}

CodeIndexPayload is the IPC payload for code_index requests.

type CodeIndexResult added in v0.4.0

type CodeIndexResult struct {
	BlobsParsed       uint64 `json:"blobs_parsed"`
	SymbolsExtracted  uint64 `json:"symbols_extracted"`
	CommentsExtracted uint64 `json:"comments_extracted"`

	// Per-stage timing in milliseconds
	IndexDurationMs   int64 `json:"index_duration_ms"`
	SymbolDurationMs  int64 `json:"symbol_duration_ms"`
	CommentDurationMs int64 `json:"comment_duration_ms"`
	TotalDurationMs   int64 `json:"total_duration_ms"`
}

CodeIndexResult is the result of a code_index operation.

type Config

type Config struct {
	// SyncIntervalRead is how often to pull changes from remote.
	SyncIntervalRead time.Duration

	// TeamContextSyncInterval is how often to sync team context repos.
	TeamContextSyncInterval time.Duration

	// DebounceWindow batches rapid changes before committing.
	DebounceWindow time.Duration

	// InactivityTimeout is how long the daemon waits without activity before exiting.
	// Zero means never exit due to inactivity.
	InactivityTimeout time.Duration

	// VersionCheckInterval is how often to check GitHub for new releases.
	VersionCheckInterval time.Duration

	// GCCheckInterval is how often to check if any workspace needs a reclone GC.
	// The actual GC cadence is per-workspace from gc_interval_days in the manifest.
	GCCheckInterval time.Duration

	// DistillInterval is how often to trigger memory distillation.
	// Zero disables automatic distillation.
	DistillInterval time.Duration

	// GitHubSyncInterval is how often to sync PRs/issues from GitHub.
	// Zero disables automatic GitHub sync.
	GitHubSyncInterval time.Duration

	// AutoStart starts daemon on first ox command if true.
	AutoStart bool

	// LedgerPath is the path to the ledger repository.
	LedgerPath string

	// ProjectRoot is the path to the project root (for loading team contexts).
	ProjectRoot string
}

Config holds daemon configuration settings.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns the default daemon configuration.

type Daemon

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

Daemon manages background ledger sync operations.

func New

func New(config *Config, logger *slog.Logger) *Daemon

New creates a new daemon instance.

func (*Daemon) RestartRequested added in v0.5.0

func (d *Daemon) RestartRequested() bool

RestartRequested returns true if the daemon stopped due to a version mismatch and should be re-executed with the updated binary.

func (*Daemon) Start

func (d *Daemon) Start() error

Start starts the daemon in the foreground. This blocks until Stop is called or a termination signal is received.

func (*Daemon) Stop

func (d *Daemon) Stop() error

Stop stops the daemon gracefully.

type DaemonInfo

type DaemonInfo struct {
	WorkspaceID   string    `json:"workspace_id"`
	WorkspacePath string    `json:"workspace_path"`
	RepoID        string    `json:"repo_id,omitempty"` // identifies repo-scoped daemons across clones
	SocketPath    string    `json:"socket_path"`
	PID           int       `json:"pid"`
	Version       string    `json:"version"`
	StartedAt     time.Time `json:"started_at"`
}

DaemonInfo represents information about a running daemon.

func FindDaemonForRepo added in v0.5.0

func FindDaemonForRepo(repoID string) *DaemonInfo

FindDaemonForRepo scans the registry for a daemon with a matching repo_id. Returns the DaemonInfo if found, nil otherwise.

func KillAllDaemons

func KillAllDaemons() ([]DaemonInfo, error)

KillAllDaemons stops all running daemons gracefully.

func ListRunningDaemons

func ListRunningDaemons() ([]DaemonInfo, error)

ListRunningDaemons returns all daemons that are actually running (socket responsive). Prunes stale entries from the registry.

type DaemonIssue

type DaemonIssue struct {
	// Type categorizes the issue.
	// Examples: "merge_conflict", "missing_scaffolding", "diverged", "auth_expiring"
	Type string `json:"type"`

	// Severity indicates urgency. No "info" level exists.
	//   - "warning": should address soon, not blocking operations
	//   - "error": blocking normal operation, agent should fix now
	//   - "critical": data at risk, urgent attention required
	Severity string `json:"severity"`

	// Repo identifies which repository has the issue.
	// Examples: "ledger", "team-context-abc123"
	// Empty string for global issues (e.g., auth expiring).
	Repo string `json:"repo,omitempty"`

	// Summary is a human-readable one-liner for display.
	// The LLM will investigate the repo directly to understand details.
	Summary string `json:"summary"`

	// Since tracks when the issue was first detected.
	// Useful for understanding how long an issue has been outstanding.
	Since time.Time `json:"since"`

	// RequiresConfirm indicates the resolution needs human approval before execution.
	// When true, the agent should propose a fix and wait for user confirmation.
	// When false, the agent can attempt to resolve automatically.
	// This separates urgency (Severity) from authority (who decides).
	RequiresConfirm bool `json:"requires_confirm,omitempty"`
}

DaemonIssue represents something the daemon cannot resolve with deterministic code. If the daemon could fix it programmatically, it already would have. These issues require LLM reasoning or human judgment to resolve.

Design principles:

  • Issue granularity is (Type, Repo), not file-level. The daemon flags that a repo has a problem; the LLM investigates the repo to understand details.
  • No "info" severity level. If something is just informational, the daemon doesn't need help - it's a notification, not a request for reasoning.
  • Severity drives CLI behavior: warning=mention, error=fix now, critical=urgent.
  • RequiresConfirm separates urgency from authority: some issues need human approval even if the agent could technically attempt a fix.

func (DaemonIssue) FormatLine

func (i DaemonIssue) FormatLine(includeSeverity bool) string

FormatLine returns a formatted single-line representation of the issue. If includeSeverity is true, includes the severity tag in brackets.

type DoctorResponse

type DoctorResponse struct {
	AntiEntropyTriggered     bool     `json:"anti_entropy_triggered"`
	ClonesTriggered          int      `json:"clones_triggered"`
	SessionFinalizeTriggered bool     `json:"session_finalize_triggered"`
	SessionFinalizeQueued    int      `json:"session_finalize_queued"`
	Errors                   []string `json:"errors,omitempty"`
}

DoctorResponse is the response for the doctor IPC message.

type ErrorStore

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

ErrorStore manages daemon errors for user notification. Errors are persisted to disk and survive daemon restarts.

Thread safety: RWMutex allows concurrent reads from IPC handlers.

func NewErrorStore

func NewErrorStore(path string) *ErrorStore

NewErrorStore creates a new error store at the given path. If path is empty, uses the default daemon state directory.

func (*ErrorStore) Add

func (e *ErrorStore) Add(err StoredError)

Add adds an error to the store. If an error with the same code already exists, it updates the existing entry. Automatically saves to disk.

func (*ErrorStore) Cleanup

func (e *ErrorStore) Cleanup(maxAge time.Duration)

Cleanup removes errors older than maxAge. Automatically saves to disk if any errors were removed.

func (*ErrorStore) Clear

func (e *ErrorStore) Clear()

Clear removes all errors. Automatically saves to disk.

func (*ErrorStore) Count

func (e *ErrorStore) Count() int

Count returns the total number of errors.

func (*ErrorStore) GetAll

func (e *ErrorStore) GetAll() []StoredError

GetAll returns all errors (viewed and unviewed). Returns a copy sorted by timestamp (most recent first).

func (*ErrorStore) GetUnviewed

func (e *ErrorStore) GetUnviewed() []StoredError

GetUnviewed returns all errors that haven't been viewed. Returns a copy sorted by timestamp (most recent first).

func (*ErrorStore) Load

func (e *ErrorStore) Load() error

Load reads the error store from disk.

func (*ErrorStore) MarkAllViewed

func (e *ErrorStore) MarkAllViewed()

MarkAllViewed marks all errors as viewed. Automatically saves to disk.

func (*ErrorStore) MarkViewed

func (e *ErrorStore) MarkViewed(id string)

MarkViewed marks an error as viewed by its ID. Automatically saves to disk.

func (*ErrorStore) Save

func (e *ErrorStore) Save() error

Save persists the error store to disk.

func (*ErrorStore) UnviewedCount

func (e *ErrorStore) UnviewedCount() int

UnviewedCount returns the number of unviewed errors.

type ExtendedStatus

type ExtendedStatus struct {
	RecentErrorCount int
	LastError        string
	LastErrorTime    string
}

ExtendedStatus provides additional status info for diagnostics.

func GetExtendedStatus

func GetExtendedStatus(s *StatusData) (ExtendedStatus, bool)

GetExtendedStatus extracts extended status from StatusData. Returns the extended status and true if available.

type FileSystem

type FileSystem interface {
	// Stat returns file info for the given path.
	Stat(name string) (fs.FileInfo, error)

	// ReadDir reads a directory and returns its entries.
	ReadDir(name string) ([]fs.DirEntry, error)
}

FileSystem abstracts filesystem operations for testability.

type FileSystemWatcher

type FileSystemWatcher interface {
	// Add adds a path to the watch list.
	Add(path string) error

	// Events returns the channel for receiving file system events.
	Events() <-chan fsnotify.Event

	// Errors returns the channel for receiving watcher errors.
	Errors() <-chan error

	// Close stops the watcher and releases resources.
	Close() error
}

FileSystemWatcher abstracts filesystem watching operations for testability. This interface allows tests to inject mock implementations that simulate file events without requiring actual filesystem operations.

func DefaultWatcherFactory

func DefaultWatcherFactory() (FileSystemWatcher, error)

DefaultWatcherFactory creates real filesystem watchers.

type FlushThrottle added in v0.3.0

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

FlushThrottle prevents thundering herd flushes by enforcing a minimum cooldown between flush operations. Without throttling, every Record() call above a batch threshold can spawn a new flush goroutine, creating unbounded HTTP POSTs.

Usage: call TryFlush() before spawning a flush goroutine. It atomically claims a flush slot if the cooldown has elapsed. Call RecordFlush() from the flush function itself (e.g., ticker-triggered flushes that bypass TryFlush).

func NewFlushThrottle added in v0.3.0

func NewFlushThrottle(cooldown time.Duration) *FlushThrottle

NewFlushThrottle creates a throttle with the given minimum interval between flushes.

func (*FlushThrottle) RecordFlush added in v0.3.0

func (ft *FlushThrottle) RecordFlush()

RecordFlush updates the last flush timestamp. Use this for flushes triggered by the background ticker (which bypass TryFlush).

func (*FlushThrottle) TryFlush added in v0.3.0

func (ft *FlushThrottle) TryFlush() bool

TryFlush atomically checks if the cooldown has elapsed and claims the flush slot. Returns true if the caller should proceed with flushing.

type FrictionCollector

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

FrictionCollector manages friction event buffering and transmission to the cloud. It delegates to a frictionax.Friction instance which handles the ring buffer, background flush loop, rate limiting, and catalog caching internally.

func NewFrictionCollector

func NewFrictionCollector(logger *slog.Logger, projectEndpoint string) *FrictionCollector

NewFrictionCollector creates a new friction event collector. If friction is disabled via settings, the collector operates as a no-op.

projectEndpoint is the project's configured endpoint (e.g., "https://test.sageox.ai"). If empty, falls back to the default production endpoint. SAGEOX_FRICTION_ENDPOINT env var always takes precedence when set.

func (*FrictionCollector) IsEnabled

func (f *FrictionCollector) IsEnabled() bool

IsEnabled returns whether friction collection is enabled.

func (*FrictionCollector) Record

func (f *FrictionCollector) Record(event friction.FrictionEvent)

Record adds a friction event to the buffer. This is non-blocking and safe for concurrent use.

func (*FrictionCollector) RecordFromIPC

func (f *FrictionCollector) RecordFromIPC(payload FrictionPayload)

RecordFromIPC adds a friction event from an IPC payload.

func (*FrictionCollector) SetAuthTokenGetter

func (f *FrictionCollector) SetAuthTokenGetter(cb func() string)

SetAuthTokenGetter sets the callback to get auth token from heartbeat cache. Friction events are only accepted by the server from authenticated users.

func (*FrictionCollector) Start

func (f *FrictionCollector) Start()

Start begins background processing of friction events. frictionax starts its background sender automatically in New(), so this is retained for API compatibility but is effectively a no-op.

func (*FrictionCollector) Stats

func (f *FrictionCollector) Stats() FrictionStats

Stats returns current friction stats for status display.

func (*FrictionCollector) Stop

func (f *FrictionCollector) Stop()

Stop gracefully shuts down the friction collector. Performs a final flush before returning. Safe to call multiple times.

type FrictionPayload

type FrictionPayload struct {
	// Timestamp in ISO8601 format (RFC3339 UTC).
	Timestamp string `json:"ts"`

	// Kind categorizes the failure type (unknown-command, unknown-flag, invalid-arg, parse-error).
	Kind string `json:"kind"`

	// Command is the top-level command.
	Command string `json:"command,omitempty"`

	// Subcommand is the subcommand if applicable.
	Subcommand string `json:"subcommand,omitempty"`

	// Actor identifies who ran the command (human or agent).
	Actor string `json:"actor"`

	// AgentType is the specific agent type when Actor is "agent" (e.g., "claude-code").
	AgentType string `json:"agent_type,omitempty"`

	// PathBucket categorizes the working directory (home, repo, other).
	PathBucket string `json:"path_bucket"`

	// Input is the redacted command input (max 500 chars).
	Input string `json:"input"`

	// ErrorMsg is the redacted, truncated error message (max 200 chars).
	ErrorMsg string `json:"error_msg"`
}

FrictionPayload is the payload for friction events from CLI. These events capture CLI usage friction (unknown commands, typos, etc.) and are forwarded to the friction analytics service.

type FrictionStats

type FrictionStats struct {
	Enabled        bool    `json:"enabled"`
	BufferCount    int     `json:"buffer_count"`
	BufferSize     int     `json:"buffer_size"`
	SampleRate     float64 `json:"sample_rate"`
	CatalogVersion string  `json:"catalog_version,omitempty"`
}

FrictionStats holds friction statistics for status display.

type GitHubSyncManager added in v0.5.0

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

GitHubSyncManager handles automatic GitHub PR/issue sync in the daemon. It fetches from the GitHub API, writes JSON files to the ledger, and commits+pushes so the data is available for distillation and cross-coworker search.

func NewGitHubSyncManager added in v0.5.0

func NewGitHubSyncManager(projectRoot string, ledgerMu *sync.Mutex, logger *slog.Logger) *GitHubSyncManager

NewGitHubSyncManager creates a new sync manager.

func (*GitHubSyncManager) CheckAndSync added in v0.5.0

func (m *GitHubSyncManager) CheckAndSync(ctx context.Context, ledgerPath string)

CheckAndSync runs a non-blocking GitHub sync if conditions are met.

func (*GitHubSyncManager) SetCodeDBManager added in v0.5.0

func (m *GitHubSyncManager) SetCodeDBManager(codedb *CodeDBManager)

SetCodeDBManager sets the CodeDB manager so indexing is triggered immediately after extraction (rather than waiting for the next 5m cycle).

func (*GitHubSyncManager) SetIssueTracker added in v0.5.0

func (m *GitHubSyncManager) SetIssueTracker(tracker *IssueTracker)

SetIssueTracker sets the issue tracker for reporting auth/sync problems.

func (*GitHubSyncManager) Status added in v0.5.0

func (m *GitHubSyncManager) Status() GitHubSyncStats

Status returns current sync status.

type GitHubSyncStats added in v0.5.0

type GitHubSyncStats struct {
	LastSync  time.Time `json:"last_sync,omitempty"`
	LastError string    `json:"last_error,omitempty"`
	Syncing   bool      `json:"syncing"`
	Owner     string    `json:"owner,omitempty"`
	Repo      string    `json:"repo,omitempty"`
}

GitHubSyncStats exposes sync status for ox status / IPC.

type HandlerResult

type HandlerResult struct {
	Response    *Response // response to send (nil = no response)
	SkipDefault bool      // if true, don't send the default response
}

HandlerResult represents the result of a message handler.

type HealthStatus

type HealthStatus int

HealthStatus represents overall daemon health.

const (
	HealthHealthy HealthStatus = iota
	HealthWarning
	HealthCritical
)

type HeartbeatCreds

type HeartbeatCreds struct {
	// Token is the git PAT (Personal Access Token) for clone/push/pull.
	// Issued by the SageOx git server, expires periodically (see ExpiresAt).
	Token string `json:"token"`

	// ServerURL is the git server base URL (e.g., "https://git.sageox.io").
	// Used to construct clone URLs for ledger and team context repos.
	ServerURL string `json:"server_url"`

	// ExpiresAt is when the git token expires.
	// Daemon uses this to trigger credential refresh before expiry.
	ExpiresAt time.Time `json:"expires_at"`

	// AuthToken is the OAuth access token for REST API calls.
	// Used to call endpoints like GET /api/v1/cli/repos to refresh git credentials.
	// This is separate from Token because git and API auth are different systems.
	AuthToken string `json:"auth_token"`

	// UserEmail is the authenticated user's email address.
	// Used for logging auth events and displaying in `ox status`.
	UserEmail string `json:"user_email"`

	// UserID is the authenticated user's unique identifier.
	// Used for telemetry and audit logging.
	UserID string `json:"user_id"`
}

HeartbeatCreds contains credentials for the daemon. Passed in heartbeats to keep daemon credentials fresh.

Two credential types are passed:

  1. Git credentials (Token/ServerURL): for clone/fetch/push operations
  2. Auth credentials (AuthToken): for REST API calls (e.g., GET /api/v1/cli/repos)

User identity (UserEmail/UserID) is included so daemon can:

  • Log authentication events (login, logout, user switch)
  • Include user context in telemetry
  • Display authenticated user in `ox status`

func (*HeartbeatCreds) Copy

func (c *HeartbeatCreds) Copy() *HeartbeatCreds

Copy returns a deep copy of HeartbeatCreds. Used to prevent races when storing credentials from external sources.

type HeartbeatEntry

type HeartbeatEntry struct {
	Timestamp     time.Time `json:"ts"`
	DaemonPID     int       `json:"pid"`
	DaemonVersion string    `json:"version"`
	Workspace     string    `json:"workspace"`
	LastSync      time.Time `json:"last_sync,omitempty"`
	Status        string    `json:"status"` // "healthy", "error", "starting"
	ErrorCount    int       `json:"error_count,omitempty"`
}

HeartbeatEntry represents a single heartbeat written to a repo.

func ReadLastHeartbeatFromPath

func ReadLastHeartbeatFromPath(heartbeatPath string) (*HeartbeatEntry, error)

ReadLastHeartbeatFromPath reads the most recent heartbeat from an explicit path. Used for reading from ~/.cache/sageox/<endpoint>/heartbeats/<id>.jsonl

type HeartbeatHandler

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

HeartbeatHandler processes incoming heartbeats from CLI commands.

func NewHeartbeatHandler

func NewHeartbeatHandler(logger *slog.Logger) *HeartbeatHandler

NewHeartbeatHandler creates a new heartbeat handler.

func (*HeartbeatHandler) GetActivitySummary

func (h *HeartbeatHandler) GetActivitySummary() ActivitySummary

GetActivitySummary returns a summary of all tracked activity.

func (*HeartbeatHandler) GetAgentActivity

func (h *HeartbeatHandler) GetAgentActivity() *ActivityTracker

GetAgentActivity returns the activity tracker for connected agents.

func (*HeartbeatHandler) GetAgentContextStats added in v0.3.0

func (h *HeartbeatHandler) GetAgentContextStats(agentID string) AgentContextStats

GetAgentContextStats returns the cumulative context consumption for a given agent.

func (*HeartbeatHandler) GetAgentPID added in v0.5.0

func (h *HeartbeatHandler) GetAgentPID(agentID string) int

GetAgentPID returns the parent process ID for a given agent. Returns 0 if not known.

func (*HeartbeatHandler) GetAgentParentID added in v0.5.0

func (h *HeartbeatHandler) GetAgentParentID(agentID string) string

GetAgentParentID returns the parent agent ID for a given agent. Returns empty string if no parent is known.

func (*HeartbeatHandler) GetAgentType added in v0.5.0

func (h *HeartbeatHandler) GetAgentType(agentID string) string

GetAgentType returns the agent type for a given agent. Returns empty string if no type is known.

func (*HeartbeatHandler) GetAuthToken

func (h *HeartbeatHandler) GetAuthToken() string

GetAuthToken returns the cached auth token for API calls. Returns empty string if no auth token is available.

func (*HeartbeatHandler) GetAuthenticatedUser

func (h *HeartbeatHandler) GetAuthenticatedUser() *AuthenticatedUser

GetAuthenticatedUser returns info about the currently authenticated user. Returns nil if no user is authenticated.

func (*HeartbeatHandler) GetCallers added in v0.5.0

func (h *HeartbeatHandler) GetCallers() []CallerInfo

GetCallers returns all known callers (clones/worktrees) that have sent heartbeats.

func (*HeartbeatHandler) GetCredentials

func (h *HeartbeatHandler) GetCredentials() (*HeartbeatCreds, time.Time)

GetCredentials returns the current credentials and their freshness. Returns nil if no credentials have been received.

func (*HeartbeatHandler) GetRepoActivity

func (h *HeartbeatHandler) GetRepoActivity() *ActivityTracker

GetRepoActivity returns the activity tracker for repos.

func (*HeartbeatHandler) GetTeamActivity

func (h *HeartbeatHandler) GetTeamActivity() *ActivityTracker

GetTeamActivity returns the activity tracker for teams.

func (*HeartbeatHandler) GetWorkspaceActivity

func (h *HeartbeatHandler) GetWorkspaceActivity() *ActivityTracker

GetWorkspaceActivity returns the activity tracker for workspaces.

func (*HeartbeatHandler) Handle

func (h *HeartbeatHandler) Handle(callerID string, payload json.RawMessage)

Handle processes an incoming heartbeat message. callerID identifies the clone/worktree that sent the heartbeat (path-based hash).

func (*HeartbeatHandler) HasValidCredentials

func (h *HeartbeatHandler) HasValidCredentials() bool

HasValidCredentials returns true if we have non-expired credentials.

func (*HeartbeatHandler) LastCallerPath added in v0.5.0

func (h *HeartbeatHandler) LastCallerPath() string

LastCallerPath returns the most recent caller clone/worktree path from heartbeats. Returns empty string if no heartbeat with CallerPath has been received.

func (*HeartbeatHandler) SetActivityCallback

func (h *HeartbeatHandler) SetActivityCallback(cb func())

SetActivityCallback sets the callback for any heartbeat activity.

func (*HeartbeatHandler) SetInitialCredentials

func (h *HeartbeatHandler) SetInitialCredentials(creds *HeartbeatCreds)

SetInitialCredentials pre-populates credentials (e.g., from credential store on startup). This allows daemon to have credentials immediately without waiting for first heartbeat.

func (*HeartbeatHandler) SetTeamNeededCallback

func (h *HeartbeatHandler) SetTeamNeededCallback(cb func(teamID string))

SetTeamNeededCallback sets the callback for when a team context is needed.

func (*HeartbeatHandler) SetVersionMismatchCallback

func (h *HeartbeatHandler) SetVersionMismatchCallback(cb func(cliVersion, daemonVersion string))

SetVersionMismatchCallback sets the callback for CLI/daemon version mismatch. Called when CLI version differs from daemon version, typically triggers restart.

type HeartbeatPayload

type HeartbeatPayload struct {
	// RepoPath identifies which git repository the CLI is operating in.
	// Used for activity tracking sparklines and to prioritize sync for active repos.
	RepoPath string `json:"repo_path,omitempty"`

	// WorkspaceID identifies the workspace context (derived from repo path hash).
	// Enables multi-workspace daemon support and request routing.
	WorkspaceID string `json:"workspace_id,omitempty"`

	// CallerPath is the absolute path of the clone/worktree sending this heartbeat.
	// With per-repo daemons, multiple clones share one daemon — CallerPath lets the
	// daemon know which clone paths are alive and keeps registry.workspace_path fresh.
	CallerPath string `json:"caller_path,omitempty"`

	// AgentID identifies the agent session (e.g., "Oxa7b3").
	// Used for tracking which agents are actively connected to the daemon.
	// Empty for non-agent CLI commands.
	AgentID string `json:"agent_id,omitempty"`

	// TeamIDs lists team contexts referenced in the current operation.
	// Triggers lazy loading of team context repos when daemon sees new team IDs.
	TeamIDs []string `json:"team_ids,omitempty"`

	// Credentials contains tokens for git operations and API calls.
	// CLI pushes credentials so daemon doesn't need filesystem access to token stores.
	// This is the primary mechanism for keeping daemon authenticated.
	Credentials *HeartbeatCreds `json:"credentials,omitempty"`

	// Timestamp is when the heartbeat was generated.
	// Used for staleness detection and activity timeline.
	Timestamp time.Time `json:"timestamp"`

	// CLIVersion is the version of the CLI sending the heartbeat.
	// Daemon compares this to its own version; mismatch triggers daemon restart
	// to ensure CLI and daemon stay in sync after upgrades.
	CLIVersion string `json:"cli_version,omitempty"`

	// ContextTokens is the estimated token count of context this command produced.
	// Accumulated per-agent by the daemon for visibility into context budget usage.
	// Zero means no context tracking for this heartbeat (e.g., non-agent commands).
	ContextTokens int64 `json:"context_tokens,omitempty"`

	// CommandName identifies which ox subcommand produced this context (e.g., "prime",
	// "team-ctx", "session list"). Used for per-command breakdown (ox-aw0).
	CommandName string `json:"command_name,omitempty"`

	// ParentAgentID is the agent ID of the parent that spawned this agent (e.g., "Oxa7b3").
	// Empty for top-level agents. Used for tree structure in `ox agent list`.
	ParentAgentID string `json:"parent_agent_id,omitempty"`

	// AgentType identifies the kind of agent (e.g., "claude-code", "explore").
	// Used for display in `ox agent list`.
	AgentType string `json:"agent_type,omitempty"`

	// ParentPID is the process ID of the parent agent process (e.g., Claude Code).
	// Captured via os.Getppid() in the CLI. Used by the daemon for instant liveness
	// detection via kill(pid, 0) instead of waiting for heartbeat timeout.
	ParentPID int `json:"parent_pid,omitempty"`
}

HeartbeatPayload is sent by CLI commands to the daemon. All fields are optional - commands send what context they have.

The heartbeat serves multiple purposes:

  1. Activity tracking: lets daemon know CLI is active (prevents inactivity shutdown)
  2. Context awareness: daemon learns which repos/teams/workspaces/agents are in use
  3. Credential refresh: CLI pushes fresh tokens so daemon can make API calls
  4. Version sync: ensures daemon and CLI are compatible

type Instance

type Instance struct {
	// ID is the unique instance identifier (e.g., "Oxa7b3").
	// Generated by the agent on startup.
	ID string `json:"id"`

	// AgentType identifies the type of agent (e.g., "claude-code", "cursor").
	AgentType string `json:"agent_type"`

	// StartTime is when the instance was registered.
	StartTime time.Time `json:"start_time"`

	// LastHeartbeat is when the last heartbeat was received.
	LastHeartbeat time.Time `json:"last_heartbeat"`

	// WorkspacePath is the workspace directory the agent is operating in.
	WorkspacePath string `json:"workspace_path"`

	// Status is the computed instance status: "active", "idle", or "stale".
	// Computed from LastHeartbeat relative to current time.
	Status string `json:"status"`

	// ParentPID is the process ID of the parent agent process.
	// Used for instant liveness detection via kill(pid, 0).
	ParentPID int `json:"parent_pid,omitempty"`
}

Instance represents an active agent instance. Instances are registered when agents connect and tracked via heartbeats.

func (*Instance) IsProcessAlive added in v0.5.0

func (i *Instance) IsProcessAlive() bool

IsProcessAlive checks if the parent agent process is still running. Uses kill(pid, 0) which checks existence without sending a signal. Returns false if no PID was recorded or the process is gone.

type InstanceInfo

type InstanceInfo struct {
	// AgentID is the short agent identifier (e.g., "Oxa7b3").
	AgentID string `json:"agent_id"`

	// WorkspacePath is the workspace/repo the agent is working in.
	WorkspacePath string `json:"workspace_path"`

	// LastHeartbeat is when the agent last sent a heartbeat.
	LastHeartbeat time.Time `json:"last_heartbeat"`

	// HeartbeatCount is the number of heartbeats received from this agent.
	HeartbeatCount int `json:"heartbeat_count"`

	// Status is "active" (recent heartbeat) or "idle" (stale heartbeat).
	Status string `json:"status"`

	// CumulativeContextTokens is the estimated total tokens of context this agent consumed from ox commands.
	CumulativeContextTokens int64 `json:"cumulative_context_tokens,omitempty"`

	// CommandCount is the number of ox commands that produced context output for this agent.
	CommandCount int `json:"command_count,omitempty"`

	// ParentAgentID is the parent agent that spawned this agent (empty for top-level agents).
	// Populated from heartbeat tracking, enabling cross-worktree tree display.
	ParentAgentID string `json:"parent_agent_id,omitempty"`

	// AgentType identifies the kind of agent (e.g., "claude-code", "explore").
	// Populated from heartbeat tracking, enabling cross-worktree type display.
	AgentType string `json:"agent_type,omitempty"`

	// ParentPID is the parent process ID of the agent.
	// Enables instant liveness detection without heartbeat timeout.
	ParentPID int `json:"parent_pid,omitempty"`
}

InstanceInfo represents an active agent instance from a daemon. Used by the instances IPC message to report connected agents.

func GetAllInstances

func GetAllInstances() ([]InstanceInfo, error)

GetAllInstances queries all running daemons and aggregates their agent instances. Returns instances from all workspaces, sorted by last heartbeat (most recent first).

type InstanceStore

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

InstanceStore manages active agent instances.

Design principles:

  • Thread-safe: sync loop and IPC handlers may access concurrently
  • Bounded: MaxInstances cap prevents unbounded memory growth
  • Self-cleaning: stale instances are automatically removed
  • Fast reads: GetActive() is O(n) where n = active instances (typically < 10)

The daemon uses InstanceStore to:

  • Track which agents are connected (for `ox status`)
  • Coordinate work across multiple agents
  • Clean up resources when agents disconnect

func NewInstanceStore

func NewInstanceStore(logger *slog.Logger) *InstanceStore

NewInstanceStore creates a new instance store.

func (*InstanceStore) ActiveCount

func (s *InstanceStore) ActiveCount() int

ActiveCount returns the number of active (non-stale) instances.

func (*InstanceStore) Count

func (s *InstanceStore) Count() int

Count returns the total number of tracked instances.

func (*InstanceStore) Deregister

func (s *InstanceStore) Deregister(id string)

Deregister removes an instance. No-op if the instance doesn't exist.

func (*InstanceStore) Get

func (s *InstanceStore) Get(id string) *Instance

Get retrieves an instance by ID. Returns nil if not found.

func (*InstanceStore) GetActive

func (s *InstanceStore) GetActive() []*Instance

GetActive returns all instances that are not stale. Includes both "active" and "idle" instances. Returns a copy sorted by last heartbeat (most recent first).

func (*InstanceStore) GetAll

func (s *InstanceStore) GetAll() []*Instance

GetAll returns all instances regardless of status. Returns a copy sorted by start time (oldest first).

func (*InstanceStore) GetStale

func (s *InstanceStore) GetStale(threshold time.Duration) []*Instance

GetStale returns instances that haven't received a heartbeat within the threshold. These are candidates for cleanup. Returns a copy sorted by last heartbeat (oldest first).

func (*InstanceStore) Heartbeat

func (s *InstanceStore) Heartbeat(id string)

Heartbeat updates the last heartbeat time for an instance. If the instance doesn't exist, this is a no-op. Use Register() to create new instances.

func (*InstanceStore) Register

func (s *InstanceStore) Register(inst *Instance)

Register adds or updates an instance. If an instance with the same ID exists, it is updated.

func (*InstanceStore) StartCleanup

func (s *InstanceStore) StartCleanup()

StartCleanup starts the background cleanup routine. Call Stop() to stop the cleanup routine.

func (*InstanceStore) Stop

func (s *InstanceStore) Stop()

Stop stops the background cleanup routine.

type InstancesResponse

type InstancesResponse struct {
	Instances []InstanceInfo `json:"instances"`
}

InstancesResponse is the response for the instances IPC message.

type IssueTracker

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

IssueTracker maintains the daemon's issue cache.

Design: The daemon detects issues during sync operations and caches them in memory. CLI reads are O(1) memory access - no blocking on git operations. This is critical because CLI commands block the agent's event loop and must be fast (< 1ms).

Thread safety: Sync loop writes, IPC handlers read. RWMutex allows concurrent reads.

Deduplication: Only one issue per (Type, Repo) combination. If the same issue is set again, it updates the existing entry (e.g., severity might change).

func NewIssueTracker

func NewIssueTracker() *IssueTracker

NewIssueTracker creates a new issue tracker.

func (*IssueTracker) Clear

func (t *IssueTracker) Clear()

Clear removes all issues.

func (*IssueTracker) ClearIssue

func (t *IssueTracker) ClearIssue(issueType, repo string)

ClearIssue removes an issue by type and repo. No-op if the issue doesn't exist.

func (*IssueTracker) ClearRepo

func (t *IssueTracker) ClearRepo(repo string)

ClearRepo removes all issues for a specific repo. Useful when a repo is removed or all its issues are resolved.

func (*IssueTracker) Count

func (t *IssueTracker) Count() int

Count returns the number of issues.

func (*IssueTracker) GetIssues

func (t *IssueTracker) GetIssues() []DaemonIssue

GetIssues returns a copy of all issues, sorted by severity (critical first). Returns a copy to prevent races with concurrent modifications.

func (*IssueTracker) MaxSeverity

func (t *IssueTracker) MaxSeverity() string

MaxSeverity returns the highest severity among all issues. Returns empty string if no issues exist.

func (*IssueTracker) NeedsHelp

func (t *IssueTracker) NeedsHelp() bool

NeedsHelp returns true if any issues exist. This is the fast-path check for CLI - just reading a length.

func (*IssueTracker) SetIssue

func (t *IssueTracker) SetIssue(issue DaemonIssue)

SetIssue adds or updates an issue. Deduplicates by (Type, Repo) - only one issue per combination exists. If an issue with the same (Type, Repo) exists, it is updated.

type MarkErrorsPayload

type MarkErrorsPayload struct {
	// IDs to mark as viewed. If empty, marks all errors as viewed.
	IDs []string `json:"ids,omitempty"`
}

MarkErrorsPayload is the payload for marking errors as viewed.

type Message

type Message struct {
	Type        string          `json:"type"`
	WorkspaceID string          `json:"workspace_id,omitempty"` // repo-scoped daemon identity
	CallerID    string          `json:"caller_id,omitempty"`    // identifies calling clone/worktree (path-based hash)
	Payload     json.RawMessage `json:"payload,omitempty"`
}

Message represents an IPC message.

type MessageHandler

type MessageHandler func(s *Server, msg Message, conn net.Conn) HandlerResult

MessageHandler handles a specific message type. It receives the server (for accessing callbacks), the message, and the connection. Returns the handler result.

type MessageRouter

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

MessageRouter routes messages to their handlers.

func NewMessageRouter

func NewMessageRouter(logger *slog.Logger) *MessageRouter

NewMessageRouter creates a new message router.

func (*MessageRouter) Handle

func (r *MessageRouter) Handle(s *Server, msg Message, conn net.Conn) (HandlerResult, bool)

Handle routes a message to its handler. Returns the handler result and whether a handler was found.

func (*MessageRouter) Register

func (r *MessageRouter) Register(msgType string, handler MessageHandler)

Register registers a handler for a message type.

type MockFileSystem

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

MockFileSystem implements FileSystem for testing.

func NewMockFileSystem

func NewMockFileSystem() *MockFileSystem

NewMockFileSystem creates a new mock filesystem for testing.

func (*MockFileSystem) AddDir

func (m *MockFileSystem) AddDir(path string, entries []string)

AddDir adds a mock directory to the filesystem.

func (*MockFileSystem) AddFile

func (m *MockFileSystem) AddFile(path string, size int64, mode fs.FileMode)

AddFile adds a mock file to the filesystem.

func (*MockFileSystem) ReadDir

func (m *MockFileSystem) ReadDir(name string) ([]fs.DirEntry, error)

ReadDir reads a directory and returns its entries.

func (*MockFileSystem) SetReadDirError

func (m *MockFileSystem) SetReadDirError(path string, err error)

SetReadDirError sets the error to return for ReadDir on a specific path.

func (*MockFileSystem) SetStatError

func (m *MockFileSystem) SetStatError(path string, err error)

SetStatError sets the error to return for Stat on a specific path.

func (*MockFileSystem) Stat

func (m *MockFileSystem) Stat(name string) (fs.FileInfo, error)

Stat returns file info for the given path.

type MockFileSystemWatcher

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

MockFileSystemWatcher implements FileSystemWatcher for testing. It allows tests to inject events and errors without real filesystem operations.

func NewMockFileSystemWatcher

func NewMockFileSystemWatcher() *MockFileSystemWatcher

NewMockFileSystemWatcher creates a new mock watcher for testing.

func (*MockFileSystemWatcher) Add

func (m *MockFileSystemWatcher) Add(path string) error

Add records the path and returns the configured error (if any).

func (*MockFileSystemWatcher) AddedPaths

func (m *MockFileSystemWatcher) AddedPaths() []string

AddedPaths returns the paths that were added to the watcher.

func (*MockFileSystemWatcher) Close

func (m *MockFileSystemWatcher) Close() error

Close closes the channels and returns the configured error (if any).

func (*MockFileSystemWatcher) CloseErrors

func (m *MockFileSystemWatcher) CloseErrors()

CloseErrors closes the errors channel.

func (*MockFileSystemWatcher) CloseEvents

func (m *MockFileSystemWatcher) CloseEvents()

CloseEvents closes the events channel to signal completion.

func (*MockFileSystemWatcher) Errors

func (m *MockFileSystemWatcher) Errors() <-chan error

Errors returns the errors channel.

func (*MockFileSystemWatcher) Events

func (m *MockFileSystemWatcher) Events() <-chan fsnotify.Event

Events returns the events channel.

func (*MockFileSystemWatcher) SendError

func (m *MockFileSystemWatcher) SendError(err error)

SendError sends an error to the watcher.

func (*MockFileSystemWatcher) SendEvent

func (m *MockFileSystemWatcher) SendEvent(event fsnotify.Event)

SendEvent sends an event to the watcher.

func (*MockFileSystemWatcher) SetAddError

func (m *MockFileSystemWatcher) SetAddError(err error)

SetAddError configures the error to return from Add().

func (*MockFileSystemWatcher) SetCloseError

func (m *MockFileSystemWatcher) SetCloseError(err error)

SetCloseError configures the error to return from Close().

type NotificationStore added in v0.5.0

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

NotificationStore tracks file changes and per-agent read cursors.

Design principles:

  • Thread-safe: daemon sync loop and IPC handlers access concurrently
  • Bounded: maxEntries cap prevents unbounded memory growth
  • Deduped: same (path, teamID) pair updates in place rather than appending
  • Primary team only: currently only records changes for the project's primary team

Cursor cleanup is driven externally by InstanceStore — when an agent is removed as stale, InstanceStore calls RemoveCursor to clean up the notification cursor. This avoids a separate cleanup goroutine and ties cursor lifetime to agent lifetime.

The daemon uses NotificationStore to:

  • Record which team context files changed after git pull
  • Let agents poll for changes since their last check
  • Detect when an agent's cursor has fallen behind the buffer

func NewNotificationStore added in v0.5.0

func NewNotificationStore(maxEntries int) *NotificationStore

NewNotificationStore creates a new notification store with the given capacity.

func (*NotificationStore) CursorCount added in v0.5.0

func (ns *NotificationStore) CursorCount() int

CursorCount returns the number of tracked agent cursors.

func (*NotificationStore) EntryCount added in v0.5.0

func (ns *NotificationStore) EntryCount() int

EntryCount returns the number of change entries in the buffer.

func (*NotificationStore) GetNotifications added in v0.5.0

func (ns *NotificationStore) GetNotifications(agentID string) ([]ChangeEntry, bool)

GetNotifications returns change entries newer than the agent's cursor. On first call for an unknown agent, auto-registers the cursor at time.Now() and returns empty (the agent should have read context during prime).

Returns (entries, stale) where stale=true means entries were evicted from the buffer since the agent last checked — some changes may have been lost.

func (*NotificationStore) RecordChanges added in v0.5.0

func (ns *NotificationStore) RecordChanges(files []string, teamID, teamName string)

RecordChanges records file changes detected after a successful git pull. Deduplicates by (path, teamID) — if the same file changes again, its timestamp updates. Entries exceeding maxEntries are evicted oldest-first.

func (*NotificationStore) RemoveCursor added in v0.5.0

func (ns *NotificationStore) RemoveCursor(agentID string)

RemoveCursor removes a specific agent's cursor. Called by InstanceStore when an agent is cleaned up as stale.

type NotificationsPayload added in v0.5.0

type NotificationsPayload struct {
	AgentID string `json:"agent_id"`
}

NotificationsPayload is the payload for notifications requests.

type NotificationsResponse added in v0.5.0

type NotificationsResponse struct {
	Files []ChangeEntry `json:"files"`
	Stale bool          `json:"stale"`
}

NotificationsResponse is the response for notifications requests.

type ProgressCallback

type ProgressCallback func(stage string, percent *int, message string)

ProgressCallback is called for each progress update during long operations. Percent is nil when unknown.

type ProgressResponse

type ProgressResponse struct {
	Progress *CheckoutProgress `json:"progress,omitempty"` // non-nil = still in progress
	Success  bool              `json:"success"`            // final result
	Error    string            `json:"error,omitempty"`
	Data     json.RawMessage   `json:"data,omitempty"`
}

ProgressResponse is a response that indicates ongoing progress.

type ProgressWriter

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

ProgressWriter allows handlers to send progress updates during long operations.

func (*ProgressWriter) WriteMessage

func (pw *ProgressWriter) WriteMessage(message string) error

WriteMessage sends a progress update with just a message (no stage or percent).

func (*ProgressWriter) WriteProgress

func (pw *ProgressWriter) WriteProgress(stage string, percent int, message string) error

WriteProgress sends a progress update with known percentage.

func (*ProgressWriter) WriteStage

func (pw *ProgressWriter) WriteStage(stage string, message string) error

WriteStage sends a progress update with stage and message (no percent).

type RealFileSystem

type RealFileSystem struct{}

RealFileSystem implements FileSystem using actual OS calls.

func (*RealFileSystem) ReadDir

func (r *RealFileSystem) ReadDir(name string) ([]fs.DirEntry, error)

ReadDir reads a directory and returns its entries.

func (*RealFileSystem) Stat

func (r *RealFileSystem) Stat(name string) (fs.FileInfo, error)

Stat returns file info for the given path.

type RealFileSystemWatcher

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

RealFileSystemWatcher implements FileSystemWatcher using fsnotify.

func NewRealFileSystemWatcher

func NewRealFileSystemWatcher() (*RealFileSystemWatcher, error)

NewRealFileSystemWatcher creates a new real filesystem watcher.

func (*RealFileSystemWatcher) Add

func (r *RealFileSystemWatcher) Add(path string) error

Add adds a path to the watch list.

func (*RealFileSystemWatcher) Close

func (r *RealFileSystemWatcher) Close() error

Close stops the watcher and releases resources.

func (*RealFileSystemWatcher) Errors

func (r *RealFileSystemWatcher) Errors() <-chan error

Errors returns the channel for receiving watcher errors.

func (*RealFileSystemWatcher) Events

func (r *RealFileSystemWatcher) Events() <-chan fsnotify.Event

Events returns the channel for receiving file system events.

type Registry

type Registry struct {
	Daemons map[string]DaemonInfo `json:"daemons"`
	// contains filtered or unexported fields
}

Registry tracks all running ox daemons on the host.

func LoadRegistry

func LoadRegistry() (*Registry, error)

LoadRegistry loads the daemon registry from disk.

func (*Registry) FindByRepoID added in v0.5.0

func (r *Registry) FindByRepoID(repoID string) *DaemonInfo

FindByRepoID returns the first daemon entry matching the given repo_id, or nil if no match is found. Empty repoID always returns nil.

func (*Registry) List

func (r *Registry) List() []DaemonInfo

List returns all registered daemons.

func (*Registry) Register

func (r *Registry) Register(info DaemonInfo) error

Register adds or updates a daemon entry.

func (*Registry) Save

func (r *Registry) Save() error

Save writes the registry to disk.

func (*Registry) Unregister

func (r *Registry) Unregister(workspaceID string) error

Unregister removes a daemon entry.

type RepoStats added in v0.4.0

type RepoStats struct {
	Name    string `json:"name"`
	Path    string `json:"path"`
	Commits int    `json:"commits"`
	Blobs   int    `json:"blobs"`
}

RepoStats tracks per-repo statistics within the index.

type Response

type Response struct {
	Success bool            `json:"success"`
	Error   string          `json:"error,omitempty"`
	Data    json.RawMessage `json:"data,omitempty"`
}

Response represents an IPC response.

type RingBuffer

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

RingBuffer stores N most recent timestamps in a circular buffer.

func (*RingBuffer) Add

func (r *RingBuffer) Add(t time.Time)

Add adds a timestamp to the ring buffer.

func (*RingBuffer) Count

func (r *RingBuffer) Count() int

Count returns the number of entries in the buffer.

func (*RingBuffer) Slice

func (r *RingBuffer) Slice() []time.Time

Slice returns all timestamps in chronological order (oldest first).

type Server

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

Server handles IPC requests from clients.

func NewServer

func NewServer(logger *slog.Logger) *Server

NewServer creates a new IPC server.

func (*Server) SetActivityCallback

func (s *Server) SetActivityCallback(cb func())

SetActivityCallback sets the callback for activity tracking.

func (*Server) SetCheckoutHandler

func (s *Server) SetCheckoutHandler(cb func(payload CheckoutPayload, progress *ProgressWriter) (*CheckoutResult, error))

SetCheckoutHandler sets the handler for checkout requests. The handler receives a ProgressWriter to send progress updates during long operations.

func (*Server) SetCodeIndexHandler added in v0.4.0

func (s *Server) SetCodeIndexHandler(cb func(payload CodeIndexPayload, progress *ProgressWriter) (*CodeIndexResult, error))

SetCodeIndexHandler sets the handler for code indexing requests. The handler receives a ProgressWriter to send progress updates during indexing.

func (*Server) SetCodeStatusHandler added in v0.4.0

func (s *Server) SetCodeStatusHandler(cb func() *CodeDBStats)

SetCodeStatusHandler sets the handler for code index status requests.

func (*Server) SetDoctorHandler

func (s *Server) SetDoctorHandler(handler func() *DoctorResponse)

SetDoctorHandler sets the doctor (health check) handler.

func (*Server) SetErrorsHandler

func (s *Server) SetErrorsHandler(onGet func() []StoredError, onMark func(ids []string))

SetErrorsHandler sets the handler for retrieving unviewed errors.

func (*Server) SetFrictionHandler

func (s *Server) SetFrictionHandler(cb func(payload FrictionPayload))

SetFrictionHandler sets the handler for friction messages. Friction events are fire-and-forget - no response is sent.

func (*Server) SetHandlers

func (s *Server) SetHandlers(onSync func() error, onStop func(), onStatus func() *StatusData)

SetHandlers sets the message handlers.

func (*Server) SetHeartbeatHandler

func (s *Server) SetHeartbeatHandler(cb func(callerID string, payload json.RawMessage))

SetHeartbeatHandler sets the handler for heartbeat messages. callerID identifies which clone/worktree sent the heartbeat (path-based hash).

func (*Server) SetInstancesHandler

func (s *Server) SetInstancesHandler(cb func() []InstanceInfo)

SetInstancesHandler sets the handler for retrieving active agent instances.

func (*Server) SetNotificationsHandler added in v0.5.0

func (s *Server) SetNotificationsHandler(cb func(agentID string) ([]ChangeEntry, bool))

SetNotificationsHandler sets the handler for notification queries.

func (*Server) SetSessionFinalizeHandler added in v0.5.0

func (s *Server) SetSessionFinalizeHandler(fn func(payload SessionFinalizeIPCPayload))

SetSessionFinalizeHandler sets the handler for session finalize messages. Session finalize events are fire-and-forget - no response is sent.

func (*Server) SetSessionsHandler

func (s *Server) SetSessionsHandler(cb func() []AgentSession)

SetSessionsHandler sets the handler for retrieving active agent sessions. Deprecated: Use SetInstancesHandler instead.

func (*Server) SetSyncHandler

func (s *Server) SetSyncHandler(cb func(progress *ProgressWriter) error)

SetSyncHandler sets the sync handler with progress support. This supersedes the onSync callback set in SetHandlers.

func (*Server) SetSyncHistoryHandler

func (s *Server) SetSyncHistoryHandler(handler func() []SyncEvent)

SetSyncHistoryHandler sets the sync history handler.

func (*Server) SetTeamSyncHandler

func (s *Server) SetTeamSyncHandler(cb func(progress *ProgressWriter) error)

SetTeamSyncHandler sets the team context sync handler with progress support.

func (*Server) SetTelemetryHandler

func (s *Server) SetTelemetryHandler(cb func(payload json.RawMessage))

SetTelemetryHandler sets the handler for telemetry messages. Telemetry is fire-and-forget - no response is sent.

func (*Server) SetTriggerGCHandler added in v0.3.0

func (s *Server) SetTriggerGCHandler(handler func() *TriggerGCResponse)

SetTriggerGCHandler sets the handler for forced GC reclone.

func (*Server) Start

func (s *Server) Start(ctx context.Context) error

Start starts the IPC server.

type SessionFinalizeIPCPayload added in v0.5.0

type SessionFinalizeIPCPayload struct {
	SessionName string `json:"session_name"` // e.g. "2026-03-12T11-09-ryan-OxTndR"
	LedgerPath  string `json:"ledger_path"`  // ledger repo root
	CachePath   string `json:"cache_path"`   // local cache session dir (source files)
	ProjectRoot string `json:"project_root"` // for endpoint/auth resolution
}

SessionFinalizeIPCPayload carries the minimum info needed for the daemon to upload and finalize a session that was saved locally by the CLI.

type SessionsResponse

type SessionsResponse struct {
	Sessions []AgentSession `json:"sessions"`
}

SessionsResponse is the response for the sessions IPC message. Deprecated: Use InstancesResponse instead.

type StatusData

type StatusData struct {
	Running          bool          `json:"running"`
	Pid              int           `json:"pid"`
	Version          string        `json:"version"`
	Uptime           time.Duration `json:"uptime"`
	WorkspacePath    string        `json:"workspace_path,omitempty"`
	LedgerPath       string        `json:"ledger_path"`
	LastSync         time.Time     `json:"last_sync"`
	SyncIntervalRead time.Duration `json:"sync_interval_read"`

	// error tracking
	RecentErrorCount int    `json:"recent_error_count,omitempty"`
	LastError        string `json:"last_error,omitempty"`
	LastErrorTime    string `json:"last_error_time,omitempty"`

	// sync insights
	TotalSyncs    int           `json:"total_syncs,omitempty"`
	SyncsLastHour int           `json:"syncs_last_hour,omitempty"`
	AvgSyncTime   time.Duration `json:"avg_sync_time,omitempty"`

	// workspaces being synced, keyed by type ("ledger", "team-context")
	// each type maps to a list of workspaces of that type (ledger has 1, team-context may have many)
	Workspaces    map[string][]WorkspaceSyncStatus `json:"workspaces,omitempty"`
	ProjectTeamID string                           `json:"project_team_id,omitempty"` // primary team for this project

	// team context sync (deprecated: use Workspaces["team-context"] instead)
	TeamContexts []TeamContextSyncStatus `json:"team_contexts,omitempty"`

	// inactivity tracking
	InactivityTimeout time.Duration `json:"inactivity_timeout,omitempty"`
	TimeSinceActivity time.Duration `json:"time_since_activity,omitempty"`

	// heartbeat activity tracking (for sparklines)
	Activity *ActivitySummary `json:"activity,omitempty"`

	// authenticated user (from heartbeat credentials)
	AuthenticatedUser *AuthenticatedUser `json:"authenticated_user,omitempty"`

	// NeedsHelp is true when the daemon has issues requiring LLM reasoning.
	// If the daemon could solve it with deterministic code, it already would have.
	// This is the fast-path check for CLI - just reading a boolean.
	NeedsHelp bool `json:"needs_help"`

	// Issues contains problems the daemon cannot resolve alone.
	// Keyed by (Type, Repo) - only one issue per combination.
	// The LLM inspects repos directly to understand details; daemon just flags repo-level issues.
	// Severity levels: "warning" (address soon), "error" (blocking), "critical" (urgent).
	// No "info" level - if daemon needs help, it's at least a warning.
	Issues []DaemonIssue `json:"issues,omitempty"`

	// UnviewedErrorCount is the number of persisted errors that haven't been viewed.
	// These are errors that persist across daemon restarts for user notification.
	UnviewedErrorCount int `json:"unviewed_error_count,omitempty"`

	// startup timing (how long the daemon took to start)
	StartupDurationMs  int64 `json:"startup_duration_ms,omitempty"`
	ThrottleDurationMs int64 `json:"throttle_duration_ms,omitempty"`

	// code index status
	CodeDB *CodeDBStats `json:"code_db,omitempty"`

	// agent work manager status
	AgentWork *agentwork.AgentWorkStatus `json:"agent_work,omitempty"`

	// connected clones/worktrees that have sent heartbeats
	Callers []CallerInfo `json:"callers,omitempty"`
}

StatusData represents daemon status information.

func (*StatusData) LastSyncForPath added in v0.3.0

func (s *StatusData) LastSyncForPath(path string) (time.Time, bool)

LastSyncForPath returns the last sync time for a workspace at the given path. Returns (time, true) if found and non-zero, (zero, false) otherwise. Safe to call on nil receiver.

type StoredError

type StoredError struct {
	ID        string    `json:"id"`
	Message   string    `json:"message"`
	Code      string    `json:"code"`
	Timestamp time.Time `json:"timestamp"`
	Viewed    bool      `json:"viewed"`
	Severity  string    `json:"severity"` // "warning", "error"
}

StoredError represents a daemon error that needs user attention. These are persisted to disk so they survive daemon restarts.

func NewStoredError

func NewStoredError(code, message string, severity string) StoredError

NewStoredError creates a new StoredError with the given code and message.

type SyncEvent

type SyncEvent struct {
	Time         time.Time     `json:"time"`
	Type         string        `json:"type"`                   // "pull", "push", "full", "team_context"
	WorkspaceID  string        `json:"workspace_id,omitempty"` // workspace that was synced (e.g., "ledger", team_id)
	Duration     time.Duration `json:"duration"`
	FilesChanged int           `json:"files_changed"`
}

SyncEvent tracks a successful sync with metadata.

type SyncMetrics

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

SyncMetrics tracks observability counters and timing for sync operations. Counters and timestamps use lock-free atomics; only pullDurations needs a mutex.

func NewSyncMetrics

func NewSyncMetrics() *SyncMetrics

NewSyncMetrics creates a new SyncMetrics instance.

func (*SyncMetrics) RecordConflict

func (m *SyncMetrics) RecordConflict()

RecordConflict records a merge conflict detection.

func (*SyncMetrics) RecordForcePush

func (m *SyncMetrics) RecordForcePush()

RecordForcePush records a force push detection.

func (*SyncMetrics) RecordPullFailure

func (m *SyncMetrics) RecordPullFailure()

RecordPullFailure records a failed pull operation.

func (*SyncMetrics) RecordPullSuccess

func (m *SyncMetrics) RecordPullSuccess(duration time.Duration)

RecordPullSuccess records a successful pull operation.

func (*SyncMetrics) RecordTeamSync

func (m *SyncMetrics) RecordTeamSync()

RecordTeamSync records a successful team context sync.

func (*SyncMetrics) RecordTeamSyncError

func (m *SyncMetrics) RecordTeamSyncError()

RecordTeamSyncError records a failed team context sync.

func (*SyncMetrics) Snapshot

func (m *SyncMetrics) Snapshot() SyncMetricsSnapshot

Snapshot returns a point-in-time copy of metrics for reporting.

type SyncMetricsSnapshot

type SyncMetricsSnapshot struct {
	PullSuccessCount   int64         `json:"pull_success_count"`
	PullFailureCount   int64         `json:"pull_failure_count"`
	ConflictCount      int64         `json:"conflict_count"`
	ForcePushCount     int64         `json:"force_push_count"`
	TeamSyncCount      int64         `json:"team_sync_count"`
	TeamSyncErrorCount int64         `json:"team_sync_error_count"`
	LastPullSuccess    time.Time     `json:"last_pull_success,omitempty"`
	LastPullFailure    time.Time     `json:"last_pull_failure,omitempty"`
	LastConflict       time.Time     `json:"last_conflict,omitempty"`
	AvgPullDuration    time.Duration `json:"avg_pull_duration"`
	P95PullDuration    time.Duration `json:"p95_pull_duration"`
}

SyncMetricsSnapshot is a point-in-time copy of sync metrics for reporting.

type SyncScheduler

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

SyncScheduler manages periodic sync operations.

func NewSyncScheduler

func NewSyncScheduler(cfg *Config, logger *slog.Logger) *SyncScheduler

NewSyncScheduler creates a new sync scheduler.

func (*SyncScheduler) Checkout

func (s *SyncScheduler) Checkout(payload CheckoutPayload, progress *ProgressWriter) (*CheckoutResult, error)

Checkout clones a repository if it doesn't exist. Sends progress updates via ProgressWriter during long operations. Uses cloneSem to bound concurrent clone operations (blocks until a slot is available). After successful clone of ledger/team-context repos, creates AGENTS.md. Checkout clones a repository to the specified path.

┌─────────────────────────────────────────────────────────────────────────────┐ │ DAEMON IPC HANDLER: checkout │ │ Classification: 🔶 CRITICAL PATH WITH FALLBACK │ │ (see docs/ai/specs/ipc-architecture.md) │ │ │ │ Clone is CRITICAL for product functionality - without it, SageOx cannot │ │ be initialized at all. However, IPC to this handler is NOT strictly │ │ required because the CLI has a FALLBACK: │ │ │ │ cmd/ox/doctor_git_repos.go:cloneViaDaemon() │ │ → Falls back to gitserver.CloneFromURLWithEndpoint() when daemon unavailable │ │ │ │ This handler is PREFERRED over direct clone because it provides: │ │ - Centralized credential handling │ │ - Progress streaming to CLI │ │ - Consistent locking for concurrent operations │ │ - AGENTS.md creation after clone │ │ - Workspace registry cache invalidation │ └─────────────────────────────────────────────────────────────────────────────┘

func (*SyncScheduler) LastError

func (s *SyncScheduler) LastError() (string, time.Time)

LastError returns the most recent error message and time.

func (*SyncScheduler) LastRemoteChange

func (s *SyncScheduler) LastRemoteChange(repoPath string) time.Time

LastRemoteChange returns the most recent FETCH_HEAD mtime for a repo. Returns zero time if no remote changes have been observed.

func (*SyncScheduler) LastSync

func (s *SyncScheduler) LastSync() time.Time

LastSync returns the timestamp of the last successful sync.

func (*SyncScheduler) LedgerMu added in v0.5.0

func (s *SyncScheduler) LedgerMu() *sync.Mutex

LedgerMu returns the shared ledger mutex for git operations.

func (*SyncScheduler) Metrics

func (s *SyncScheduler) Metrics() *SyncMetrics

Metrics returns the sync metrics for observability.

func (*SyncScheduler) RecentErrorCount

func (s *SyncScheduler) RecentErrorCount() int

RecentErrorCount returns the count of recent errors (last hour).

func (*SyncScheduler) RemoteChangeActivity

func (s *SyncScheduler) RemoteChangeActivity() *ActivityTracker

RemoteChangeActivity returns the remote change tracker for status display.

func (*SyncScheduler) SetActivityCallback

func (s *SyncScheduler) SetActivityCallback(cb func())

SetActivityCallback sets the callback for activity tracking.

func (*SyncScheduler) SetAgentWorkSignal added in v0.5.0

func (s *SyncScheduler) SetAgentWorkSignal(ch chan<- struct{})

SetAgentWorkSignal sets the channel used to notify the agent work manager after a successful ledger pull.

func (*SyncScheduler) SetAuthTokenGetter

func (s *SyncScheduler) SetAuthTokenGetter(cb func() string)

SetAuthTokenGetter sets the callback to get auth token from heartbeat cache. Used for lazy credential refresh via /api/v1/cli/repos.

func (*SyncScheduler) SetCodeDBManager added in v0.4.0

func (s *SyncScheduler) SetCodeDBManager(m *CodeDBManager)

SetCodeDBManager sets the CodeDB manager for periodic freshness checks.

func (*SyncScheduler) SetGitHubSyncManager added in v0.5.0

func (s *SyncScheduler) SetGitHubSyncManager(m *GitHubSyncManager)

SetGitHubSyncManager sets the GitHub sync manager for periodic PR/issue sync.

func (*SyncScheduler) SetIssueTracker

func (s *SyncScheduler) SetIssueTracker(tracker *IssueTracker)

SetIssueTracker sets the issue tracker for reporting sync issues. Issues are reported when the daemon encounters problems it cannot resolve with deterministic code (e.g., merge conflicts requiring LLM reasoning).

func (*SyncScheduler) SetNotificationStore added in v0.5.0

func (s *SyncScheduler) SetNotificationStore(store *NotificationStore)

SetNotificationStore sets the notification store for team context change tracking.

func (*SyncScheduler) SetTelemetryCallback

func (s *SyncScheduler) SetTelemetryCallback(cb func(syncType, operation, status string, duration time.Duration))

SetTelemetryCallback sets the callback for telemetry events. Called when sync operations complete with syncType, operation, status, and duration.

func (*SyncScheduler) Start

func (s *SyncScheduler) Start(ctx context.Context)

Start starts the sync scheduler.

func (*SyncScheduler) Sync

func (s *SyncScheduler) Sync() error

Sync performs an immediate full sync. Used for manual requests via IPC.

func (*SyncScheduler) SyncHistory

func (s *SyncScheduler) SyncHistory() []SyncEvent

SyncHistory returns recent sync events for display.

func (*SyncScheduler) SyncStats

func (s *SyncScheduler) SyncStats() SyncStatistics

SyncStats returns aggregate statistics about recent syncs.

func (*SyncScheduler) SyncWithProgress

func (s *SyncScheduler) SyncWithProgress(progress *ProgressWriter) error

SyncWithProgress performs a full sync with progress updates. If progress is nil, no progress updates are sent. Returns an error if the ledger sync fails (surfaced to CLI via IPC).

func (*SyncScheduler) TeamContextStatus

func (s *SyncScheduler) TeamContextStatus() []TeamContextSyncStatus

TeamContextStatus returns the current team context sync status. Uses the WorkspaceRegistry for a unified view of workspace state.

func (*SyncScheduler) TeamSync

func (s *SyncScheduler) TeamSync(progress *ProgressWriter) error

TeamSync performs an on-demand sync of all team contexts with progress updates.

func (*SyncScheduler) TriggerAntiEntropy

func (s *SyncScheduler) TriggerAntiEntropy()

TriggerAntiEntropy triggers self-healing checks for missing workspaces. This is called by IPC when doctor or other commands want to ensure ledgers and team contexts are cloned.

func (*SyncScheduler) TriggerGC added in v0.3.0

func (s *SyncScheduler) TriggerGC(ctx context.Context) *TriggerGCResponse

TriggerGC forces a GC reclone of all eligible team contexts, bypassing the interval check. Returns immediately if GC is already in progress. Runs synchronously.

func (*SyncScheduler) TriggerSync

func (s *SyncScheduler) TriggerSync()

TriggerSync triggers an immediate sync (debounced by watcher).

func (*SyncScheduler) WorkspaceRegistry

func (s *SyncScheduler) WorkspaceRegistry() *WorkspaceRegistry

WorkspaceRegistry returns the workspace registry for status queries.

type SyncState added in v0.3.0

type SyncState struct {
	LastSync            time.Time `json:"last_sync"`
	LastSyncCommit      string    `json:"last_sync_commit"`
	ConsecutiveFailures int       `json:"consecutive_failures"`
}

SyncState tracks the sync health of a workspace (team context or ledger). Stored in .sageox/cache/sync-state.json within the workspace directory.

This is local-only machine state — never committed or pushed to the remote. Protected by .sageox/.gitignore (cache/ is gitignored).

func LoadSyncState added in v0.3.0

func LoadSyncState(workspacePath string) *SyncState

LoadSyncState reads sync state from .sageox/cache/sync-state.json within workspacePath. Returns empty SyncState (not error) if the file is missing or corrupt.

func (*SyncState) IsStale added in v0.3.0

func (s *SyncState) IsStale(threshold time.Duration) bool

IsStale returns true if the last successful sync exceeds the given threshold. A zero LastSync (never synced) is always stale.

func (*SyncState) RecordFailure added in v0.3.0

func (s *SyncState) RecordFailure()

RecordFailure increments the consecutive failure count.

func (*SyncState) RecordSuccess added in v0.3.0

func (s *SyncState) RecordSuccess(commitSHA string)

RecordSuccess updates state after a successful sync.

func (*SyncState) StaleDuration added in v0.3.0

func (s *SyncState) StaleDuration() time.Duration

StaleDuration returns how long since the last successful sync. Returns 0 if never synced.

type SyncStatistics

type SyncStatistics struct {
	TotalSyncs    int
	SyncsLastHour int
	AvgDuration   time.Duration
	OldestSync    time.Time
	NewestSync    time.Time
}

SyncStatistics holds aggregate sync metrics.

type TeamContextSyncStatus

type TeamContextSyncStatus struct {
	TeamID   string    `json:"team_id"`
	TeamName string    `json:"team_name"`
	Path     string    `json:"path"`
	CloneURL string    `json:"clone_url,omitempty"` // git remote URL
	LastSync time.Time `json:"last_sync"`
	LastErr  string    `json:"last_error,omitempty"`
	Exists   bool      `json:"exists"` // whether the local path exists
}

TeamContextSyncStatus tracks sync status for a team context repo.

type TelemetryCollector

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

TelemetryCollector manages event buffering and transmission to the cloud. It uses a ring buffer for bounded memory usage and supports server-controlled throttling via X-SageOx-Interval header.

func NewTelemetryCollector

func NewTelemetryCollector(logger *slog.Logger) *TelemetryCollector

NewTelemetryCollector creates a new telemetry collector. It loads or generates a persistent client ID and checks opt-out settings.

func (*TelemetryCollector) IsEnabled

func (c *TelemetryCollector) IsEnabled() bool

IsEnabled returns whether telemetry collection is enabled.

func (*TelemetryCollector) Record

func (c *TelemetryCollector) Record(event string, props map[string]any)

Record adds an event to the ring buffer. This is non-blocking and safe for concurrent use.

func (*TelemetryCollector) RecordCodeIndexComplete added in v0.5.0

func (c *TelemetryCollector) RecordCodeIndexComplete(result *CodeIndexResult, status string)

RecordCodeIndexComplete records a code index completion event with per-stage timing.

func (*TelemetryCollector) RecordDaemonCrash

func (c *TelemetryCollector) RecordDaemonCrash(uptime time.Duration, errType, errMsg string)

RecordDaemonCrash records a daemon crash/panic event.

func (*TelemetryCollector) RecordDaemonShutdown

func (c *TelemetryCollector) RecordDaemonShutdown(uptime time.Duration, reason string)

RecordDaemonShutdown records the daemon shutdown event.

func (*TelemetryCollector) RecordDaemonStartup

func (c *TelemetryCollector) RecordDaemonStartup()

RecordDaemonStartup records the daemon startup event.

func (*TelemetryCollector) RecordFromIPC

func (c *TelemetryCollector) RecordFromIPC(event string, props map[string]any)

RecordFromIPC records an event received via IPC from CLI. The props should already contain app_type from the CLI.

func (*TelemetryCollector) RecordSyncComplete

func (c *TelemetryCollector) RecordSyncComplete(syncType, operation, status string, duration time.Duration, recordsCount int)

RecordSyncComplete records a sync completion event.

func (*TelemetryCollector) Start

func (c *TelemetryCollector) Start()

Start begins background processing of telemetry events.

func (*TelemetryCollector) Stats

Stats returns current telemetry stats for status display.

func (*TelemetryCollector) Stop

func (c *TelemetryCollector) Stop()

Stop gracefully shuts down the telemetry collector. Performs a final flush before returning. Safe to call multiple times.

type TelemetryEvent

type TelemetryEvent struct {
	UUID    string         `json:"uuid"`
	TS      int64          `json:"ts"`
	TSLocal string         `json:"tslocal"`
	Event   string         `json:"event"`
	Props   map[string]any `json:"props"`
}

TelemetryEvent matches the spec format for telemetry events.

type TelemetryPayload

type TelemetryPayload struct {
	Event string         `json:"event"` // event name (e.g., "sync:complete")
	Props map[string]any `json:"props"` // event properties
}

TelemetryPayload is the payload for telemetry events from CLI.

type TelemetryStats

type TelemetryStats struct {
	Enabled      bool          `json:"enabled"`
	BufferCount  int           `json:"buffer_count"`
	BufferSize   int           `json:"buffer_size"`
	SendInterval time.Duration `json:"send_interval"`
	LastSend     time.Time     `json:"last_send"`
	ClientID     string        `json:"client_id"`
}

TelemetryStats holds telemetry statistics for status display.

type TriggerGCResponse added in v0.3.0

type TriggerGCResponse struct {
	Triggered int      `json:"triggered"`
	Skipped   int      `json:"skipped,omitempty"`
	Errors    []string `json:"errors,omitempty"`
}

TriggerGCResponse is the response for trigger_gc requests. Errors include both failures (clone/validation errors) and skips due to uncommitted changes — GC is a disk-space optimization and must never destroy user work.

type VersionCache added in v0.2.0

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

VersionCache manages the GitHub release version cache on disk. Thread-safe for concurrent access.

func NewVersionCache added in v0.2.0

func NewVersionCache(log *slog.Logger) *VersionCache

NewVersionCache creates a new version cache. The cache file is stored at ~/.cache/sageox/version-check.json (or XDG equivalent).

func (*VersionCache) CheckAndUpdate added in v0.2.0

func (v *VersionCache) CheckAndUpdate(ctx context.Context) error

CheckAndUpdate fetches the latest release from GitHub and updates the cache. Uses ETag conditional requests to avoid unnecessary data transfer. Safe to call concurrently.

func (*VersionCache) Data added in v0.2.0

func (v *VersionCache) Data() *VersionCacheData

Data returns a copy of the cached version data. Returns nil if no data is cached. Safe to call concurrently.

func (*VersionCache) Load added in v0.2.0

func (v *VersionCache) Load() error

Load reads the version cache from disk if it exists. Returns nil error if file doesn't exist (empty cache is valid). Safe to call concurrently.

func (*VersionCache) Save added in v0.2.0

func (v *VersionCache) Save(data *VersionCacheData) error

Save writes the version cache to disk atomically. Creates the cache directory if it doesn't exist. Safe to call concurrently.

type VersionCacheData added in v0.2.0

type VersionCacheData struct {
	LatestVersion string    `json:"latest_version"`
	CheckedAt     time.Time `json:"checked_at"`
	ETag          string    `json:"etag,omitempty"`
}

VersionCacheData holds the cached latest release version from GitHub.

type Watcher

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

Watcher monitors the ledger directory for changes.

func NewWatcher

func NewWatcher(path string, debounceWindow time.Duration, logger *slog.Logger) *Watcher

NewWatcher creates a new file watcher.

func NewWatcherWithFS

func NewWatcherWithFS(
	path string,
	debounceWindow time.Duration,
	logger *slog.Logger,
	watcherFactory WatcherFactory,
	fileSystem FileSystem,
) *Watcher

NewWatcherWithFS creates a new file watcher with injectable dependencies. This constructor is primarily for testing, allowing injection of mock filesystem and watcher implementations.

func (*Watcher) Start

func (w *Watcher) Start(ctx context.Context, onChange func())

Start starts watching for file changes. Calls the callback when changes are detected (debounced).

func (*Watcher) Stop

func (w *Watcher) Stop()

Stop stops any pending debounce timer and prevents future callbacks. Safe to call multiple times.

type WatcherFactory

type WatcherFactory func() (FileSystemWatcher, error)

WatcherFactory creates FileSystemWatcher instances. This abstraction allows tests to inject mock watchers.

type WorkspaceRegistry

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

WorkspaceRegistry tracks all workspaces (ledger + team contexts) for a daemon. Provides a unified view of workspace state for both daemon lifecycle and sync operations.

Design rationale: - Single source of truth for workspace state (replaces duplicate tracking in sync.go) - Caches loaded config to avoid repeated disk reads - Adds runtime state (Exists, LastErr, SyncInProgress) on top of config - Thread-safe for concurrent access from daemon goroutines

INVARIANT: WorkspaceRegistry is the sole writer to config.local.toml within the daemon. All config writes must go through registry methods (UpdateConfigLastSync, PersistLedgerPath, etc.) to prevent cache/disk divergence. Never call config.SaveLocalConfig directly from sync.go or other daemon code.

func NewWorkspaceRegistry

func NewWorkspaceRegistry(projectRoot, repoName string) *WorkspaceRegistry

NewWorkspaceRegistry creates a new workspace registry for the given project.

func (*WorkspaceRegistry) CleanupRevokedTeamContexts

func (r *WorkspaceRegistry) CleanupRevokedTeamContexts(currentTeamIDs map[string]bool)

CleanupRevokedTeamContexts removes team contexts that are no longer in the repo detail API response. Only removes team contexts that were discovered via the detail API (public TCs for non-members). Team contexts discovered from user credentials (member teams) are never removed by this cleanup, since the detail API is project-scoped and doesn't include all user teams.

func (*WorkspaceRegistry) ClearCloneRetry

func (r *WorkspaceRegistry) ClearCloneRetry(id string)

ClearCloneRetry clears the clone retry state after a successful clone.

func (*WorkspaceRegistry) ClearSyncFailures added in v0.1.1

func (r *WorkspaceRegistry) ClearSyncFailures(id string)

ClearSyncFailures resets sync retry state after a successful sync.

func (*WorkspaceRegistry) ClearWorkspaceError

func (r *WorkspaceRegistry) ClearWorkspaceError(id string)

ClearWorkspaceError clears the error for a workspace.

func (*WorkspaceRegistry) GetAllWorkspaces

func (r *WorkspaceRegistry) GetAllWorkspaces() []WorkspaceState

GetAllWorkspaces returns copies of all workspaces (ledger + team contexts).

func (*WorkspaceRegistry) GetCloneRetryInfo

func (r *WorkspaceRegistry) GetCloneRetryInfo(id string) (attempts int, nextAttempt time.Time)

GetCloneRetryInfo returns the current clone retry state for a workspace. Returns attempts=0 if no retry state exists.

func (*WorkspaceRegistry) GetEndpoint

func (r *WorkspaceRegistry) GetEndpoint() string

GetEndpoint returns the API endpoint from project config.

func (*WorkspaceRegistry) GetGCInterval added in v0.3.0

func (r *WorkspaceRegistry) GetGCInterval(path string) int

GetGCInterval returns the GC interval for a workspace. Returns manifest.DefaultGCIntervalDays (7) if not set or workspace not found.

func (*WorkspaceRegistry) GetLastGCTime added in v0.3.0

func (r *WorkspaceRegistry) GetLastGCTime(id string) time.Time

GetLastGCTime returns when GC last ran for a workspace.

func (*WorkspaceRegistry) GetLedger

func (r *WorkspaceRegistry) GetLedger() *WorkspaceState

GetLedger returns a copy of the ledger workspace state. Returns nil if no ledger workspace exists.

func (*WorkspaceRegistry) GetLedgerPath

func (r *WorkspaceRegistry) GetLedgerPath() string

GetLedgerPath returns the ledger path for quick access.

func (*WorkspaceRegistry) GetRepoID

func (r *WorkspaceRegistry) GetRepoID() string

GetRepoID returns the repo ID from project config. Used for API calls like GetLedgerStatus.

func (*WorkspaceRegistry) GetSyncIntervalMin added in v0.3.0

func (r *WorkspaceRegistry) GetSyncIntervalMin(path string) int

GetSyncIntervalMin returns the manifest-derived sync interval for a workspace identified by its local path. Returns 0 if not set or workspace not found.

func (*WorkspaceRegistry) GetSyncRetryInfo added in v0.1.1

func (r *WorkspaceRegistry) GetSyncRetryInfo(id string) (failures int, nextAttempt time.Time)

GetSyncRetryInfo returns the current sync retry state for a workspace.

func (*WorkspaceRegistry) GetTeamContextStatus

func (r *WorkspaceRegistry) GetTeamContextStatus() []TeamContextSyncStatus

GetTeamContextStatus returns team context status in the legacy format. This provides backward compatibility with existing status display code.

func (*WorkspaceRegistry) GetTeamContexts

func (r *WorkspaceRegistry) GetTeamContexts() []WorkspaceState

GetTeamContexts returns copies of all team context workspaces.

func (*WorkspaceRegistry) GetWorkspace

func (r *WorkspaceRegistry) GetWorkspace(id string) *WorkspaceState

GetWorkspace returns a copy of a workspace by ID. Returns nil if the workspace doesn't exist.

func (*WorkspaceRegistry) HasFetchHead

func (r *WorkspaceRegistry) HasFetchHead(id string) (exists bool, mtime time.Time)

HasFetchHead checks if the workspace has a .git/FETCH_HEAD file and returns its modification time.

func (*WorkspaceRegistry) InitializeLedger

func (r *WorkspaceRegistry) InitializeLedger(cloneURL, projectRoot string)

InitializeLedger creates a ledger workspace from API-fetched URL. Called when ledger URL is fetched from API but no ledger workspace exists yet.

Path is computed via config.DefaultLedgerPath (user directory) for consistency.

func (*WorkspaceRegistry) InvalidateConfigCache

func (r *WorkspaceRegistry) InvalidateConfigCache()

InvalidateConfigCache forces the next LoadFromConfig to reload from disk.

func (*WorkspaceRegistry) LoadFromConfig

func (r *WorkspaceRegistry) LoadFromConfig() error

LoadFromConfig loads workspace state from config.local.toml. Uses cached config if recently loaded, otherwise reloads from disk.

func (*WorkspaceRegistry) PersistLedgerPath added in v0.1.1

func (r *WorkspaceRegistry) PersistLedgerPath(path string) error

PersistLedgerPath saves the ledger path to the config cache and disk. This keeps the cache in sync so UpdateConfigLastSync doesn't overwrite it.

func (*WorkspaceRegistry) ProjectTeamID added in v0.3.0

func (r *WorkspaceRegistry) ProjectTeamID() string

ProjectTeamID returns the project's primary team ID.

func (*WorkspaceRegistry) RecordSyncAttempt

func (r *WorkspaceRegistry) RecordSyncAttempt(id string)

RecordSyncAttempt records that a sync was attempted for a workspace.

func (*WorkspaceRegistry) RecordSyncFailure added in v0.1.1

func (r *WorkspaceRegistry) RecordSyncFailure(id string)

RecordSyncFailure increments the consecutive failure count and sets backoff. Backoff: 1min, 2min, 4min, 8min, 16min, 32min→capped to 30min. Creates a minimal workspace entry if one doesn't exist yet.

func (*WorkspaceRegistry) RefreshExists

func (r *WorkspaceRegistry) RefreshExists()

RefreshExists updates the Exists field for all workspaces.

func (*WorkspaceRegistry) RegisterTeamContextsFromAPI

func (r *WorkspaceRegistry) RegisterTeamContextsFromAPI(teamContexts []api.RepoDetailTeamContext)

RegisterTeamContextsFromAPI registers team contexts discovered from the repo detail API. This is the third discovery source: public team contexts visible to non-members. Only adds new team contexts that aren't already tracked (from config or credentials).

func (*WorkspaceRegistry) SetCloneRetry

func (r *WorkspaceRegistry) SetCloneRetry(id string, attempts int, nextAttempt time.Time)

SetCloneRetry records a failed clone attempt with exponential backoff. attempts is the total number of failed attempts (1-based). nextAttempt is when the next clone should be attempted.

func (*WorkspaceRegistry) SetGCInterval added in v0.3.0

func (r *WorkspaceRegistry) SetGCInterval(path string, days int)

SetGCInterval stores the manifest-derived GC interval for a workspace.

func (*WorkspaceRegistry) SetLedgerCloneURL

func (r *WorkspaceRegistry) SetLedgerCloneURL(cloneURL string) bool

SetLedgerCloneURL sets the clone URL for the ledger workspace. Called when ledger URL is fetched from API. Returns false if no ledger workspace exists (caller should ensure ledger is initialized first).

func (*WorkspaceRegistry) SetSyncInProgress

func (r *WorkspaceRegistry) SetSyncInProgress(id string, inProgress bool)

SetSyncInProgress marks a workspace as syncing.

func (*WorkspaceRegistry) SetSyncIntervalMin added in v0.3.0

func (r *WorkspaceRegistry) SetSyncIntervalMin(path string, minutes int)

SetSyncIntervalMin stores the manifest-derived sync interval for a workspace identified by its local path. Uses path lookup since callers may not have the workspace ID readily available.

func (*WorkspaceRegistry) SetWorkspaceError

func (r *WorkspaceRegistry) SetWorkspaceError(id, errMsg string)

SetWorkspaceError records an error for a workspace.

func (*WorkspaceRegistry) ShouldRetryClone

func (r *WorkspaceRegistry) ShouldRetryClone(id string) bool

ShouldRetryClone checks if enough time has passed to retry a failed clone. Returns true if: - No previous clone attempts (first try) - Current time is after NextCloneAttempt (backoff expired)

func (*WorkspaceRegistry) ShouldSync added in v0.1.1

func (r *WorkspaceRegistry) ShouldSync(id string) bool

ShouldSync checks if enough time has passed since the last sync failure to retry. Returns true if no previous failures or backoff has expired.

func (*WorkspaceRegistry) UpdateConfigLastSync

func (r *WorkspaceRegistry) UpdateConfigLastSync(id string) error

UpdateConfigLastSync updates the last sync time in both registry and config file. This should be called after a successful sync.

func (*WorkspaceRegistry) UpdateLastGC added in v0.3.0

func (r *WorkspaceRegistry) UpdateLastGC(id string)

UpdateLastGC records that GC completed for a workspace.

type WorkspaceState

type WorkspaceState struct {
	// identity
	ID   string        `json:"id"`   // workspace ID (ledger=path hash, team=team_id)
	Type WorkspaceType `json:"type"` // ledger or team_context
	Path string        `json:"path"` // local path to the git repo

	// team-specific (only for team_context type)
	TeamID   string `json:"team_id,omitempty"`
	TeamName string `json:"team_name,omitempty"`
	TeamSlug string `json:"team_slug,omitempty"` // kebab-case slug for CLI identifiers
	CloneURL string `json:"clone_url,omitempty"` // git clone URL (from credentials)

	// config (from config.local.toml)
	Endpoint       string    `json:"endpoint,omitempty"`
	ConfigLastSync time.Time `json:"config_last_sync,omitempty"` // last sync recorded in config

	// runtime state
	Exists          bool      `json:"exists"`                      // whether the local path exists
	LastErr         string    `json:"last_error,omitempty"`        // last error during sync
	LastSyncAttempt time.Time `json:"last_sync_attempt,omitempty"` // last time we tried to sync
	SyncInProgress  bool      `json:"sync_in_progress,omitempty"`  // currently syncing

	// clone retry state (for background clones that fail)
	CloneAttempts    int       `json:"clone_attempts,omitempty"`     // number of failed clone attempts
	NextCloneAttempt time.Time `json:"next_clone_attempt,omitempty"` // when to retry clone (exponential backoff)

	// sync (fetch/pull) retry state — backoff on consecutive failures
	SyncFailures    int       `json:"sync_failures,omitempty"`     // consecutive failed sync attempts
	NextSyncAttempt time.Time `json:"next_sync_attempt,omitempty"` // when to retry sync (exponential backoff)

	// manifest-derived settings (from .sageox/sync.manifest in the team context repo)
	SyncIntervalMin int       `json:"sync_interval_min,omitempty"` // minutes between syncs (0 = use default)
	GCIntervalDays  int       `json:"gc_interval_days,omitempty"`  // days between reclones (0 = default 7)
	LastGCTime      time.Time `json:"last_gc_time,omitempty"`      // when last GC reclone completed
}

WorkspaceState represents the runtime state of a workspace (repo). Combines config data with runtime sync status.

type WorkspaceSyncStatus

type WorkspaceSyncStatus struct {
	ID             string    `json:"id"`                         // workspace ID (e.g., "ledger", team_id)
	Type           string    `json:"type"`                       // "ledger" or "team_context"
	Path           string    `json:"path"`                       // local filesystem path
	CloneURL       string    `json:"clone_url,omitempty"`        // git remote URL
	Exists         bool      `json:"exists"`                     // whether path exists locally
	TeamID         string    `json:"team_id,omitempty"`          // team ID (for team contexts)
	TeamName       string    `json:"team_name,omitempty"`        // team name (for team contexts)
	TeamSlug       string    `json:"team_slug,omitempty"`        // kebab-case team slug
	LastSync       time.Time `json:"last_sync,omitempty"`        // last successful sync
	LastErr        string    `json:"last_error,omitempty"`       // last error message
	Syncing        bool      `json:"syncing,omitempty"`          // currently syncing
	LastGCTime     time.Time `json:"last_gc_time,omitempty"`     // last successful GC reclone
	GCIntervalDays int       `json:"gc_interval_days,omitempty"` // configured GC cadence (0 = default 7)
}

WorkspaceSyncStatus represents the sync status of a workspace (ledger or team context). Provides a unified view of all repos the daemon is syncing.

type WorkspaceType

type WorkspaceType string

WorkspaceType identifies the type of workspace.

const (
	WorkspaceTypeLedger      WorkspaceType = "ledger"
	WorkspaceTypeTeamContext WorkspaceType = "team_context"
)

Directories

Path Synopsis
Package testutil provides testing utilities for daemon functionality.
Package testutil provides testing utilities for daemon functionality.

Jump to

Keyboard shortcuts

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