Documentation
¶
Index ¶
- Constants
- func FindSessionByLeafUUID(transcriptDir, leafUUID, excludeFile string) string
- type Backend
- type Capabilities
- type Chunk
- type ChunkMetadata
- type ChunkRequest
- type ChunkResponse
- type Client
- func (c *Client) Capabilities() (Capabilities, error)
- func (c *Client) Init(providerName, externalID, transcriptPath string, metadata *InitMetadata) (*InitResponse, error)
- func (c *Client) LinkGitHub(sessionID string, req *GitHubLinkRequest) (*GitHubLinkResponse, error)
- func (c *Client) SendEvent(sessionID, eventType string, timestamp time.Time, payload json.RawMessage) error
- func (c *Client) UpdateSessionSummary(externalID, summary string) error
- func (c *Client) UploadChunk(sessionID, fileName, fileType string, firstLine int, lines []string, ...) (int, error)
- type CodexRolloutMetadata
- type Engine
- func (e *Engine) GetSyncStats() map[string]int
- func (e *Engine) Init() error
- func (e *Engine) IsInitialized() bool
- func (e *Engine) OpencodeChildFilesAllowed() bool
- func (e *Engine) Reset()
- func (e *Engine) SendSessionEnd(hookInput *types.ClaudeHookInput, timestamp time.Time) error
- func (e *Engine) SessionID() string
- func (e *Engine) SetDescendantRegistrar(reg provider.DescendantRegistrar)
- func (e *Engine) SyncAll() (int, error)
- func (e *Engine) Tracker() *FileTracker
- type EngineConfig
- type EventRequest
- type EventResponse
- type FileState
- type FileTracker
- func (t *FileTracker) AddCodexRollout(path, fileName string, isRoot bool, meta CodexRolloutMetadata) *TrackedFile
- func (t *FileTracker) DiscoverNewFiles(newAgentIDs []string) []*TrackedFile
- func (t *FileTracker) GetTrackedFiles() []*TrackedFile
- func (t *FileTracker) GetTranscriptFile() *TrackedFile
- func (t *FileTracker) HasFileChanged(file *TrackedFile) bool
- func (t *FileTracker) InitFromBackendState(backendFiles map[string]FileState)
- func (t *FileTracker) IsTracked(fileName string) bool
- func (t *FileTracker) ReadChunk(file *TrackedFile, r *redactor.Redactor, maxBytes int) (*Chunk, error)
- func (t *FileTracker) RegisterCodexRollout(path, fileName string, isRoot bool, meta provider.CodexRolloutMetadata)
- func (t *FileTracker) RegisterSidechainFile(path, name, fileType string) bool
- func (t *FileTracker) RootTranscriptPath() string
- func (t *FileTracker) SubagentsDir() string
- func (t *FileTracker) UpdateAfterSync(file *TrackedFile, lastLine int, newOffset int64)
- type GitHubLinkRequest
- type GitHubLinkResponse
- type InitMetadata
- type InitRequest
- type InitResponse
- type TrackedFile
- type UpdateSummaryRequest
- type UpdateSummaryResponse
Constants ¶
const DefaultMaxChunkBytes = 14 * 1024 * 1024 // 14MB
DefaultMaxChunkBytes is the default maximum size of a chunk in bytes. This is a backend-imposed limit: the server rejects chunks larger than 16MB. We use 14MB to leave headroom for JSON encoding overhead and compression. If the backend limit changes, this constant must be updated accordingly.
Variables ¶
This section is empty.
Functions ¶
func FindSessionByLeafUUID ¶
FindSessionByLeafUUID searches transcript files in the given directory for a message with the specified uuid in the last N lines. Returns the session ID (filename without extension) if found, empty string otherwise.
Excludes the file specified by excludeFile from the search.
Types ¶
type Backend ¶ added in v0.16.0
type Backend interface {
Init(providerName, externalID, transcriptPath string, metadata *InitMetadata) (*InitResponse, error)
UploadChunk(sessionID, fileName, fileType string, firstLine int, lines []string, metadata *ChunkMetadata) (int, error)
SendEvent(sessionID, eventType string, timestamp time.Time, payload json.RawMessage) error
UpdateSessionSummary(externalID, summary string) error
// Capabilities probes the backend's optional-feature signal (CF-533).
// Returns an error (404 / network / parse) when the backend does not
// advertise capabilities; the engine treats a 404 as a definitive
// "unsupported" and other errors as transient.
Capabilities() (Capabilities, error)
}
Backend is the sync transport used by Engine. The HTTP client implements this for provider-aware backend sync.
type Capabilities ¶ added in v0.16.2
type Capabilities struct {
// WorkflowFiles reports that the backend resolves path-encoded workflow
// subagent file_names (subagents/workflows/<runId>/agent-<id>.jsonl).
WorkflowFiles bool `json:"workflow_files"`
// WorkflowJournal reports that the backend accepts the workflow_journal
// file_type (subagents/workflows/<runId>/journal.jsonl).
WorkflowJournal bool `json:"workflow_journal"`
// OpencodeSubagentFiles reports that the backend resolves path-encoded
// OpenCode subagent file_names (opencode/<child-id>/messages.jsonl) and
// stitches them into the root session's analytics (CF-538/CF-539).
OpencodeSubagentFiles bool `json:"opencode_subagent_files"`
}
Capabilities is the backend's optional-feature signal (CF-533). The response body of GET /api/v1/capabilities IS this map (no outer wrapper). Absent fields default to false (zero value), so a backend that omits a field — or the whole endpoint (404) — is treated as not supporting it.
type Chunk ¶
type Chunk struct {
FileName string // Base name of the file
FileType string // "transcript" or "agent"
FirstLine int // 1-based line number of first line
Lines []string // The lines (redacted if applicable)
NewOffset int64 // Byte offset after reading these lines
Metadata *ChunkMetadata // Metadata to send to backend
AgentIDs []string // Agent IDs discovered (local use only, not sent to backend)
}
Chunk represents a range of lines read from a file with extracted metadata
type ChunkMetadata ¶
type ChunkMetadata struct {
GitInfo *git.GitInfo `json:"git_info,omitempty"`
Summary string `json:"summary,omitempty"`
FirstUserMessage string `json:"first_user_message,omitempty"`
CodexRollout *CodexRolloutMetadata `json:"codex_rollout,omitempty"`
// LatestMessageAt carries an explicit session timestamp for providers
// whose transcript lines have no per-line timestamp (Cursor). The backend
// feeds session.last_message_at from this on transcript chunks, since it
// opts Cursor out of per-line timestamp extraction. Serialized RFC3339
// (time.Time default) to match the backend's *time.Time reader; omitted
// when nil. Cursor sets it from the transcript file mtime.
LatestMessageAt *time.Time `json:"latest_message_at,omitempty"`
// Model names the LLM that produced this session (Cursor only; sourced
// from the sessionStart hook payload). Cursor JSONL carries no model field,
// so this is the only model signal. Set engine-side from daemon config on
// transcript chunks; omitted when empty, so other providers send nothing.
// NOTE: accepted on the wire but not yet persisted by the backend (pending
// a confab-web migration), so it is currently forward-looking/inert.
Model string `json:"model,omitempty"`
}
ChunkMetadata contains metadata sent to the backend with a chunk
type ChunkRequest ¶
type ChunkRequest struct {
SessionID string `json:"session_id"`
FileName string `json:"file_name"`
FileType string `json:"file_type"`
FirstLine int `json:"first_line"`
Lines []string `json:"lines"`
Metadata *ChunkMetadata `json:"metadata,omitempty"`
}
ChunkRequest is the request body for POST /api/v1/sync/chunk
type ChunkResponse ¶
type ChunkResponse struct {
LastSyncedLine int `json:"last_synced_line"`
}
ChunkResponse is the response for POST /api/v1/sync/chunk
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client handles communication with the sync API endpoints
func NewClient ¶
func NewClient(cfg *config.UploadConfig) (*Client, error)
NewClient creates a new sync API client
func (*Client) Capabilities ¶ added in v0.16.2
func (c *Client) Capabilities() (Capabilities, error)
Capabilities probes GET /api/v1/capabilities (public, no auth). Any failure — 404 on an older backend, a network error, or a malformed body — is returned to the caller, which treats it as "no capabilities". The 404 sentinel (http.ErrSessionNotFound) is preserved via %w for errors.Is.
func (*Client) Init ¶
func (c *Client) Init(providerName, externalID, transcriptPath string, metadata *InitMetadata) (*InitResponse, error)
Init initializes or resumes a sync session Returns the session ID and current sync state for all files. The providerName must be a canonical provider name (callers via Engine.Init pass e.provider.Name(), which is always non-empty).
func (*Client) LinkGitHub ¶
func (c *Client) LinkGitHub(sessionID string, req *GitHubLinkRequest) (*GitHubLinkResponse, error)
LinkGitHub creates a GitHub link for a session
func (*Client) SendEvent ¶
func (c *Client) SendEvent(sessionID, eventType string, timestamp time.Time, payload json.RawMessage) error
SendEvent sends a session lifecycle event to the backend
func (*Client) UpdateSessionSummary ¶
UpdateSessionSummary updates the summary for a session identified by its external_id
func (*Client) UploadChunk ¶
func (c *Client) UploadChunk(sessionID, fileName, fileType string, firstLine int, lines []string, metadata *ChunkMetadata) (int, error)
UploadChunk uploads a chunk of lines for a file with optional metadata Returns the new last synced line number
type CodexRolloutMetadata ¶ added in v0.16.0
type CodexRolloutMetadata = provider.CodexRolloutMetadata
CodexRolloutMetadata is the per-rollout metadata transmitted on the FIRST chunk of a Codex rollout. The canonical definition lives in pkg/provider so the Codex implementation can construct one without an import cycle; pkg/sync re-exports it here as an alias so existing call sites that reference sync.CodexRolloutMetadata keep working. Wire format unchanged.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the core sync engine used by both daemon and manual save. It provides a unified interface for syncing session data to the backend.
func New ¶
func New(uploadCfg *config.UploadConfig, engineCfg EngineConfig) (*Engine, error)
New creates a new sync engine with the given configuration. The engine is not connected to the backend until Init() is called.
func NewWithBackend ¶ added in v0.16.0
NewWithBackend creates an engine with a preconfigured backend. Test-facing; returns an error if the provider name is invalid.
func (*Engine) GetSyncStats ¶
GetSyncStats returns current sync statistics (lines synced per file)
func (*Engine) Init ¶
Init initializes the sync session with the backend. - Creates session if not exists, or resumes existing - Gets last_synced_line for all known files - Sends initial metadata (git info, hostname, username) Must be called before SyncAll.
func (*Engine) IsInitialized ¶
IsInitialized returns true if Init() has been called successfully
func (*Engine) OpencodeChildFilesAllowed ¶ added in v0.17.0
OpencodeChildFilesAllowed reports whether OpenCode subagent sidechain files may be uploaded to this backend, per its cached capabilities (CF-538/CF-539). Lazy-probes once per SyncAll cycle; cached definitive answers persist for the engine's lifetime. Exported because the daemon's opencodeRegistrar gates RegisterOpencodeChild on it.
func (*Engine) Reset ¶
func (e *Engine) Reset()
Reset clears the initialized state, allowing Init to be called again. This is useful when the backend returns an auth error and we need to re-authenticate and re-initialize.
func (*Engine) SendSessionEnd ¶
SendSessionEnd sends a session_end event to the backend
func (*Engine) SetDescendantRegistrar ¶ added in v0.17.0
func (e *Engine) SetDescendantRegistrar(reg provider.DescendantRegistrar)
SetDescendantRegistrar overrides the default DescendantRegistrar (the engine's own *FileTracker) that Engine.SyncAll passes to provider.DiscoverDescendants. Used by the daemon to inject a registrar that wraps the FileTracker with provider-specific behavior (OpenCode child collector spawn). Must be called before SyncAll.
The honest-but-mutable setter is the cleanest break of the cyclic dependency between the engine and an OpenCode registrar: the registrar needs the engine (for the capability gate); the engine needs the registrar (for SyncAll). Construction order is engine → registrar → setter; either constructor injection or hidden closures would just reshuffle the same mutation.
func (*Engine) SyncAll ¶
SyncAll syncs all tracked files to the backend using BFS traversal. It discovers agent files referenced in transcripts and syncs them transitively within a single call. Each file is processed at most once per call.
Algorithm:
- Start with all currently tracked files in the queue
- Process each file in queue (sync if changed, extract agent IDs)
- Discover new files from collected agent IDs
- Add only NEW files to the queue for next iteration
- Repeat until queue is empty (or max iterations reached)
Returns number of chunks uploaded and the first error encountered (if any). Continues syncing other files even if one file fails.
func (*Engine) Tracker ¶ added in v0.17.0
func (e *Engine) Tracker() *FileTracker
Tracker returns the engine's internal FileTracker. Exposed so the daemon can wrap it in a provider-specific DescendantRegistrar (OpenCode) and then hand the wrapper back via SetDescendantRegistrar. Direct callers outside that wiring should not mutate the returned tracker.
type EngineConfig ¶
type EngineConfig struct {
Provider string
ExternalID string
TranscriptPath string
CWD string
// Model is the session-constant LLM model name (Cursor only; sourced from
// the sessionStart hook payload). Empty for other providers. The engine
// stamps it onto transcript chunk metadata when non-empty.
Model string
}
EngineConfig holds configuration for creating an Engine
type EventRequest ¶
type EventRequest struct {
SessionID string `json:"session_id"`
EventType string `json:"event_type"`
Timestamp time.Time `json:"timestamp"`
Payload json.RawMessage `json:"payload"`
}
EventRequest is the request body for POST /api/v1/sync/event
type EventResponse ¶
type EventResponse struct {
Success bool `json:"success"`
}
EventResponse is the response for POST /api/v1/sync/event
type FileState ¶
type FileState struct {
LastSyncedLine int `json:"last_synced_line"`
}
FileState represents the sync state for a single file from the backend
type FileTracker ¶
type FileTracker struct {
// contains filtered or unexported fields
}
FileTracker tracks files and their sync state for a session
func NewFileTracker ¶
func NewFileTracker(transcriptPath string) *FileTracker
NewFileTracker creates a new file tracker for a session
func (*FileTracker) AddCodexRollout ¶ added in v0.16.0
func (t *FileTracker) AddCodexRollout(path, fileName string, isRoot bool, meta CodexRolloutMetadata) *TrackedFile
AddCodexRollout registers a Codex rollout file in the tracker.
isRoot=true → file type "transcript" (the Codex root's primary rollout). isRoot=false → file type "agent" (every descendant, at any depth).
All descendants sync as sidechain files under the root's backend session — the same primitive Claude Code uses for its `agent-*.jsonl` files — while the Codex thread tree is preserved separately in the backend's `codex_rollouts` table via `meta.ParentThreadUUID`.
Idempotent: a second call for an already-tracked path returns the existing TrackedFile without modifying it. The caller can use this to avoid maintaining a separate "already added" set.
func (*FileTracker) DiscoverNewFiles ¶
func (t *FileTracker) DiscoverNewFiles(newAgentIDs []string) []*TrackedFile
DiscoverNewFiles checks for new agent files based on agent IDs discovered in previous chunk reads, and also scans the subagents directory for any agent files not already tracked. Returns newly discovered files.
func (*FileTracker) GetTrackedFiles ¶
func (t *FileTracker) GetTrackedFiles() []*TrackedFile
GetTrackedFiles returns all currently tracked files
func (*FileTracker) GetTranscriptFile ¶
func (t *FileTracker) GetTranscriptFile() *TrackedFile
GetTranscriptFile returns the transcript file being tracked
func (*FileTracker) HasFileChanged ¶
func (t *FileTracker) HasFileChanged(file *TrackedFile) bool
HasFileChanged checks if a file has more data to sync. Returns true if: - The file has grown (more bytes than our last known offset) - The file has been modified (mod time changed) - We haven't read the file yet (no byte offset)
func (*FileTracker) InitFromBackendState ¶
func (t *FileTracker) InitFromBackendState(backendFiles map[string]FileState)
InitFromBackendState initializes the tracker with state from the backend. This sets up tracking for the transcript and any files the backend knows about.
Called from both Engine.Init() (first time) and Engine.refreshStateFromBackend() (after a chunk-upload failure). On refresh, any per-file metadata that the engine has already set on a tracked file (notably Codex rollout metadata) must survive — otherwise a retried first chunk would lose its codex_rollout payload. We preserve CodexRollout for existing entries and only refresh the fields that can legitimately drift (sync position).
func (*FileTracker) IsTracked ¶
func (t *FileTracker) IsTracked(fileName string) bool
IsTracked returns true if a file is already being tracked
func (*FileTracker) ReadChunk ¶
func (t *FileTracker) ReadChunk(file *TrackedFile, r *redactor.Redactor, maxBytes int) (*Chunk, error)
ReadChunk reads new lines from a file starting after LastSyncedLine. Uses ByteOffset to seek directly to the right position if available. Applies redaction if a redactor is provided. Stops reading when accumulated bytes would exceed maxBytes (aligned to line boundary). Returns nil if there are no new lines.
func (*FileTracker) RegisterCodexRollout ¶ added in v0.16.0
func (t *FileTracker) RegisterCodexRollout(path, fileName string, isRoot bool, meta provider.CodexRolloutMetadata)
RegisterCodexRollout is the provider.DescendantRegistrar entry point: thin wrapper around AddCodexRollout that drops the *TrackedFile return (kept on AddCodexRollout for in-package callers that want it).
func (*FileTracker) RegisterSidechainFile ¶ added in v0.17.0
func (t *FileTracker) RegisterSidechainFile(path, name, fileType string) bool
RegisterSidechainFile is the entry point for registering path-encoded sidechain files. CF-533 uses it for Claude workflow subagent transcripts + run journals; CF-538 uses it for OpenCode subagent messages. name is the forward-slash path-encoded backend file_name; path is its absolute location on disk; fileType is "agent" or provider.FileTypeWorkflowJournal.
Returns true when a new file is tracked. When the name is already tracked (e.g. a stale entry rebuilt from backend state on daemon restart), it overwrites Path+Type in place and returns false, preserving the sync position (LastSyncedLine/ByteOffset) so the file resumes incrementally.
func (*FileTracker) RootTranscriptPath ¶ added in v0.17.3
func (t *FileTracker) RootTranscriptPath() string
RootTranscriptPath returns the absolute path of the session's root transcript file. Part of provider.RootTranscriptProvider: providers whose subagent layout differs from Claude's (e.g. Cursor) derive their subagents directory from this path instead of from SubagentsDir().
func (*FileTracker) SubagentsDir ¶ added in v0.16.2
func (t *FileTracker) SubagentsDir() string
SubagentsDir returns the <session>/subagents directory for this tracker. Part of provider.WorkflowRegistrar: the Claude provider scans subagents/workflows/<runId>/ beneath it to discover workflow files.
NOTE: this is computed for Claude's layout (transcript at <project>/<session-id>.jsonl → subagents at <project>/<session-id>/subagents/). Cursor's subagents sit beside the transcript file (<dir>/agent-transcripts/<id>/subagents/, i.e. filepath.Dir(transcript)/subagents), so Cursor's DiscoverDescendants derives the dir from RootTranscriptPath() rather than relying on this value.
func (*FileTracker) UpdateAfterSync ¶
func (t *FileTracker) UpdateAfterSync(file *TrackedFile, lastLine int, newOffset int64)
UpdateAfterSync updates the tracked file state after a successful sync. This updates both the sync position and the cached file stats (modtime/size) so HasFileChanged won't re-trigger until the file actually changes again.
type GitHubLinkRequest ¶
type GitHubLinkRequest struct {
URL string `json:"url"`
Title string `json:"title,omitempty"`
Source string `json:"source"` // "cli_hook" or "manual"
}
GitHubLinkRequest is the request body for POST /api/v1/sessions/{id}/github-links
type GitHubLinkResponse ¶
type GitHubLinkResponse struct {
ID int64 `json:"id"`
LinkType string `json:"link_type"` // "commit" or "pull_request"
URL string `json:"url"`
Owner string `json:"owner"`
Repo string `json:"repo"`
Ref string `json:"ref"`
}
GitHubLinkResponse is the response for POST /api/v1/sessions/{id}/github-links
type InitMetadata ¶
type InitMetadata struct {
CWD string `json:"cwd,omitempty"`
GitInfo json.RawMessage `json:"git_info,omitempty"`
Hostname string `json:"hostname,omitempty"`
Username string `json:"username,omitempty"`
}
InitMetadata contains optional metadata for session initialization
type InitRequest ¶
type InitRequest struct {
Provider string `json:"provider"`
ExternalID string `json:"external_id"`
TranscriptPath string `json:"transcript_path"`
Metadata *InitMetadata `json:"metadata,omitempty"`
}
InitRequest is the request body for POST /api/v1/sync/init
type InitResponse ¶
type InitResponse struct {
SessionID string `json:"session_id"`
Files map[string]FileState `json:"files"`
}
InitResponse is the response for POST /api/v1/sync/init
type TrackedFile ¶
type TrackedFile struct {
Path string // Full path to the file
Name string // Base name of the file
Type string // "transcript" or "agent"
LastSyncedLine int // Last line number synced to backend (1-based)
ByteOffset int64 // Byte position after LastSyncedLine (for seeking)
LastModTime time.Time // Last modification time (for change detection)
LastSize int64 // Last known size (for change detection)
// CodexRollout, if non-nil, marks this tracked file as a Codex rollout
// for which the engine should emit `codex_rollout` chunk metadata on
// the FIRST chunk uploaded for this file. "First chunk" is detected
// via chunk.FirstLine == 1; no separate state field is required.
// Roots and descendants both carry this; only the engine's emission
// gate (FirstLine==1) determines when it goes on the wire.
CodexRollout *CodexRolloutMetadata
}
TrackedFile represents a file being synced
func (*TrackedFile) SetCodexRollout ¶ added in v0.16.0
func (f *TrackedFile) SetCodexRollout(meta *provider.CodexRolloutMetadata)
SetCodexRollout assigns Codex rollout metadata to this tracked file, used by provider.Codex.InitTranscript via the provider.TranscriptRegistrar interface. Separated from direct field assignment so the engine and providers don't need to share struct internals.
type UpdateSummaryRequest ¶
type UpdateSummaryRequest struct {
Summary string `json:"summary"`
}
UpdateSummaryRequest is the request body for PATCH /api/v1/sessions/{external_id}/summary
type UpdateSummaryResponse ¶
type UpdateSummaryResponse struct {
Status string `json:"status"`
}
UpdateSummaryResponse is the response for PATCH /api/v1/sessions/{external_id}/summary