provider

package
v0.16.2 Latest Latest
Warning

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

Go to latest
Published: Jun 5, 2026 License: MIT Imports: 24 Imported by: 0

README

pkg/provider

Provider-specific local behavior for Confab integrations. Current providers: Claude Code and Codex. Each concrete provider owns paths, hook parsing, session discovery, and transcript metadata extraction.

The package defines a Provider interface and a HookInput interface (Phase 1 + 2 of the abstraction work — see CF-394). Both concrete provider types satisfy Provider; hook-input adapters in hookinput.go satisfy HookInput. As of CF-397 (Phase 3), pkg/sync/engine.go dispatches sync-loop behavior (root metadata, descendant discovery, chunk annotation) through the interface; as of CF-398 (Phase 4), session discovery (ScanSessions, FindSessionByID, ExtractMetadata, DefaultCWD) is also routed through the interface. cmd/ has no discovery-related provider-name branches.

Files

File Role
provider.go Provider and HookInput interfaces, sync-loop interfaces (TranscriptRegistrar, DescendantRegistrar, WorkflowRegistrar, ChunkView), SummaryLink / AnnotationResult types, provider name constants (NameClaudeCode, NameCodex), the FileTypeWorkflowJournal file-type constant, the registry (Get(name)), and NormalizeName(name)
detect.go DetectInstalled() []string returns the canonical names of providers whose CLI binary is on PATH, in fixed registry order. Uses the exported package-level LookPath var (defaults to exec.LookPath) so tests can stub it. Backs confab setup auto-detect (CF-422) and confab status per-provider CLI presence.
session.go SessionInfo and SessionMetadata — cross-provider shapes returned by the discovery interface methods. Also defines maxLinesForExtraction and the shared readHeadLines helper.
codex_rollout.go CodexRolloutMetadata — wire-format metadata transmitted on the first chunk of every Codex rollout. Lives here (not pkg/sync) so the Codex implementation can construct one without a cycle; pkg/sync aliases it.
hookinput.go claudeHookInputAdapter and codexHookInputAdapter — wrap the typed structs in pkg/types so they satisfy HookInput. Required because the structs' existing exported SessionID field collides with a SessionID() method
claude.go ClaudeCode — paths, transcript validation, parent-process detection, and the Provider methods. Sync-loop methods are no-ops except AnnotateChunk, which delegates to ExtractMetadata to extract summary + first user message + summary links from transcript chunks. Hook install/uninstall delegates to pkg/hookconfig; skill install/uninstall/status delegates to pkg/config
claude_discovery.go Claude session scanning (ScanSessions, FindSessionByID) and metadata extraction (ExtractMetadata, DefaultCWD). Walks ~/.claude/projects/, parses Claude transcript JSONL for summaries + first user messages, sanitizes HTML, truncates to maxMetadataFieldSize/2 bytes.
claude_agentids.go ClaudeCode.ExtractAgentIDsFromMessage and IsValidAgentID — Claude-only transcript-schema parsing for sidechain agent file discovery. Called from pkg/sync/tracker.go during chunk reads.
claude_workflows.go ClaudeCode.DiscoverWorkflowFiles (CF-533) — scans <session>/subagents/workflows/<runId>/ for workflow subagent transcripts + run journals and registers them via provider.WorkflowRegistrar with path-encoded backend names. workflowFileType classifies each file (agent / workflow_journal / skip). Unlike classic subagents, workflow agents have no agentId in the main transcript, so they are found by directory scan, not by ExtractAgentIDsFromMessage.
codex.go Codex — paths, transcript validation, parent-process detection, hook handling, and the Provider methods. InitTranscript attaches root rollout metadata from session_meta; DiscoverDescendants walks the SQLite subtree; DiscoverWorkflowFiles is a no-op (no Codex equivalent — the predicate is never invoked, so a Codex session never probes capabilities); AnnotateChunk attaches codex_rollout on FirstLine==1 and extracts first_user_message once per session via ExtractMetadata. Hook install/uninstall delegates to pkg/hookconfig; skill install/uninstall/status delegates to pkg/config
codex_discovery.go Codex rollout discovery: ScanSessions (interface), ScanCodexSessions (rich type), FindSessionByID (walks subagent UUIDs up to the root), package-private rollout resolution, ReadSessionInfo, ExtractFirstUserMessageFromLines, ExtractMetadata, DefaultCWD. Also houses CodexSessionInfo and the rollout-filename regex.
codex_state.go Codex local SQLite reader: StateDBPath(), WalkUpToRoot(threadUUID), ListSubtree(rootUUID). Used by the hook handler, confab save (via FindSessionByID's walk-up), and Codex.DiscoverDescendants to discover subagent rollouts and route them to the top-most root

Provider surfaces

ClaudeCode
  • Paths: StateDir, SettingsPath, ProjectsDir, transcript path validation against CONFAB_CLAUDE_DIR.
  • Discovery: ScanSessions, FindSessionByID, ExtractMetadata, DefaultCWD (the four Provider interface methods); plus ExtractAgentIDsFromMessage for classic sidechain agent file discovery and DiscoverWorkflowFiles for Workflow-tool subagent transcripts + run journals (directory-scanned, capability-gated — see claude_workflows.go and CF-533).
  • Hooks: ReadHookInput, ReadSessionHookInput, InstallHooks/UninstallHooks/IsHooksInstalled (delegate to pkg/hookconfig, which edits ~/.claude/settings.json).
  • Skills: InstallSkills installs /retro under ~/.claude/skills/ (and prunes retired skills); UninstallSkills removes bundled skills; IsSkillInstalled reports per-skill state (delegates to pkg/config).
  • Hook response: WriteHookResponse writes a types.ClaudeHookResponse.
  • Parent detection: parent PID monitoring helpers, Claude-specific.
Codex
  • Paths: StateDir (override via CONFAB_CODEX_DIR), SessionsDir, ConfigPath.
  • Discovery: ScanSessions, FindSessionByID (walks subagent UUIDs up to the root), ExtractMetadata, DefaultCWD (the four Provider interface methods).
  • Additional rollout helpers: ScanCodexSessions (rich CodexSessionInfo form), ReadSessionInfo, SessionIDFromRolloutPath, ExtractFirstUserMessageFromLines, internal walkRollouts helper.
  • Filtering: CodexSessionInfo.IsUserSession() excludes subagents/memory rollouts by thread_source and agent_* metadata.
  • Hooks: ReadHookInput, ReadSessionHookInput, InstallHooks/UninstallHooks/IsHooksInstalled (delegate to pkg/hookconfig, which edits ~/.codex/config.toml). Installs SessionStart, PreToolUse, and PostToolUse; shutdown remains parent-PID driven.
  • Skills: InstallSkills installs /retro under ~/.codex/skills/ (and prunes retired skills); UninstallSkills removes bundled skills; IsSkillInstalled reports per-skill state (delegates to pkg/config).
  • Hook response: WriteHookResponse writes a types.CodexHookResponse.
  • Parent detection: FindParentPID, IsProcess, MatchesProcess (regex (?i)\bcodex\b) for daemon parent-liveness monitoring, mirroring ClaudeCode.
  • Transcript metadata: ExtractFirstUserMessageFromLines reads the first event_msg.user_message from rollout lines, trims whitespace, and truncates to types.MaxFirstUserMessageLength on a UTF-8 boundary.
  • Path validation: ValidateRolloutPath requires an absolute path under SessionsDir matching rollout-<timestamp>-<uuid>.jsonl.
Codex daemon shutdown

Codex fires Stop at every agent/turn boundary, including root rollout stops while the interactive Codex session is still alive. Wiring confab hook session-end to [[hooks.Stop]] would therefore kill the root sync daemon prematurely. Instead:

  • Codex.InstallHooks writes [[hooks.SessionStart]], [[hooks.PreToolUse]], and [[hooks.PostToolUse]] into the managed block.
  • cmd/spawn.go stores Codex.FindParentPID() on the daemon at spawn time.
  • The daemon's main loop (pkg/daemon/daemon.go) monitors that PID and shuts down when the interactive Codex process exits — same mechanism Claude Code uses.
  • confab hook session-end --provider codex is rejected with an explicit error pointing users at their ~/.codex/config.toml.
  • Local state DB (codex_state.go): reads Codex's ~/.codex/state_*.sqlite (read-only, highest numeric suffix wins; CONFAB_CODEX_STATE_DB overrides). WalkUpToRoot(threadUUID) walks the thread_spawn_edges chain to the top-most root with a 5×50ms retry budget for the spawn-vs-edge race (and a thread_source='user' fast-path that skips retries for known roots). ListSubtree(rootUUID) returns every descendant via a recursive CTE. All paths degrade gracefully when the DB is unavailable — callers see (threadUUID, "", nil) for WalkUpToRoot and a nil slice for ListSubtree.

Provider interface

Methods every provider must implement:

  • Name() string — canonical name (one of NameClaudeCode, NameCodex).
  • CLIBinaryName() string — OS-level binary name used by DetectInstalled / confab status ("claude" for Claude Code, "codex" for Codex). Distinct from Name() because the canonical name (claude-code) is not the binary name.
  • StateDir() (string, error) — local state directory.
  • FindParentPID() int, IsProcess(pid int) bool — parent-process detection.
  • ParseSessionHook(io.Reader) (HookInput, error) — read a SessionStart hook payload and return the provider-agnostic view.
  • InstallHooks() (string, error) / UninstallHooks() (string, error) / IsHooksInstalled() (bool, error) — install/check the full hook set the provider requires. Claude installs 4 bundles (sync, PreToolUse, PostToolUse, UserPromptSubmit). Codex installs 3 events (SessionStart, PreToolUse, PostToolUse). Both methods delegate to pkg/hookconfig.
  • SupportsCommitLinking() bool — true if the provider installs the PreToolUse + PostToolUse events that drive bidirectional GitHub linking. Used by cmd/hook_pretooluse.go and cmd/hook_posttooluse.go to silently no-op for any future provider that doesn't yet support the flow. Both Claude Code and Codex return true.
  • InstallSkills() error / UninstallSkills() error / IsSkillInstalled(name string) bool — manage bundled Confab skills in the provider's local skill layout.
  • WalkUpToRoot(sessionID string) (rootID, rootPath string, error) — Codex walks thread_spawn_edges; Claude is identity with empty rootPath.
  • ShouldSpawnForInput(in HookInput) bool — Codex returns false for subagent rollouts and for unreadable rollout files; Claude always returns true. os.IsNotExist is treated as a race-tolerance "spawn anyway" case.
  • WriteHookResponse(w, suppressOutput, systemMessage) error — write the provider-specific hook response JSON (ClaudeHookResponse vs CodexHookResponse).
  • InitTranscript(target TranscriptRegistrar, transcriptPath, externalID string) error — called from sync.Engine.Init after the tracker is initialized. Codex attaches root rollout metadata via target.SetCodexRollout; Claude is a no-op. Implementations never surface read failures as errors — they log warn and fall through.
  • DiscoverDescendants(reg DescendantRegistrar, externalID string) error — called once per SyncAll cycle, before the BFS loop. Codex walks the SQLite subtree and calls reg.RegisterCodexRollout per verified descendant. Claude is a no-op (its agents are discovered transitively from transcript content inside tracker.DiscoverNewFiles). Must be idempotent across calls.
  • DiscoverWorkflowFiles(reg WorkflowRegistrar, allow func(fileType string) bool) (int, error) — called once per SyncAll cycle (CF-533). Claude scans subagents/workflows/<runId>/ and registers agent transcripts + run journals via reg.RegisterWorkflowFile under path-encoded names, gating each file on allow(fileType) (the engine's per-flag capability predicate). The provider invokes allow only after finding a candidate file, so non-workflow sessions never trigger a backend probe. Codex is a no-op. Returns the count of newly-registered files; idempotent across calls.
  • AnnotateChunk(c ChunkView, sentFirstUserMessage bool, redact func(string) string) AnnotationResult — called for every chunk before upload. Providers attach chunk-level metadata via setters on c; summary links go in the returned AnnotationResult.SummaryLinks so the engine drives the HTTP. The redact closure is nil-safe and lets providers stay decoupled from pkg/redactor. Both providers' implementations delegate to ExtractMetadata for the parsing work.
  • ScanSessions() ([]SessionInfo, error) — returns user-initiated sessions discoverable on disk, oldest first. Claude walks ~/.claude/projects/; Codex projects from ScanCodexSessions and extracts FirstUserMessage per rollout for the list-command title.
  • FindSessionByID(partialID string) (id, transcriptPath string, error) — resolves a full or partial ID. Claude is identity walk-up; Codex walks subagent UUIDs up to the root via WalkUpToRoot so callers transparently upload the whole tree.
  • ExtractMetadata(lines []string) SessionMetadata — in-memory parsing of the first maxLinesForExtraction (50) JSONL lines. Claude returns full Summary + FirstUserMessage + SummaryLinks; Codex returns only FirstUserMessage.
  • DefaultCWD(transcriptPath string) string — CWD to record on the upload. Claude returns filepath.Dir(transcriptPath); Codex reads session_meta.cwd with the dir fallback.

Get(name) and the registry

Get(name) returns the registered Provider for a canonical name (empty string defaults to claude-code). NormalizeName(name) is the same lookup but returns the canonical name string. The registry is a package-level read-only map populated at init time — to add a new provider, add its instance to the map and implement the interface.

Invariants

  • NameClaudeCode and NameCodex are the canonical wire values. Backend session uniqueness is (user_id, provider, external_id).
  • NormalizeName(name) returns claude-code for empty input (legacy default) and rejects unknown providers.
  • ClaudeStateDirEnv is duplicated between pkg/config/paths.go and pkg/provider/claude.go to break a circular import. The two MUST stay in sync; reviewers should catch any drift.
  • ClaudeCode preserves existing Claude Code behavior, including CONFAB_CLAUDE_DIR.
  • Claude hook parsing returns types.ClaudeHookInput; Codex hook parsing returns types.CodexHookInput. There is no generic normalized hook payload.
  • Codex.ExtractFirstUserMessageFromLines only considers event_msg.user_message — the first response_item.message[role=user] line in a Codex rollout contains an <environment_context> wrapper, not the user's prompt, and must be skipped.
  • truncateUTF8Bytes never returns a string longer than maxBytes, even on invalid UTF-8 input.
  • Codex.IsUserSession filters out subagents and memory rollouts so ScanSessions only surfaces top-level user sessions.
  • Codex.InstallHooks is idempotent and never strips unmanaged Codex config sections.
  • Codex.WalkUpToRoot is the single point that converts a firing thread UUID to its top-most root. All Codex daemon spawning and confab save invocations route through it, so subagent rollouts always upload under the root's session — never as orphan sessions.
  • Codex.WalkUpToRoot never returns the empty string for the root UUID; on any failure mode (no DB, schema mismatch, edge-race exhausted) it returns the input thread UUID so callers can keep moving.
  • Parent PID detection is part of the Provider interface (FindParentPID, IsProcess); the bodies remain provider-specific (different process-name patterns) and share the package-level getProcCmdline / getParentPID helpers in claude.go.
  • Agent-ID extraction (ClaudeCode.ExtractAgentIDsFromMessage) is intentionally Claude-only. Codex tracks subagents via its SQLite thread tree and never grows agent IDs in rollout JSONL — pkg/sync/tracker.go calls the Claude method on every chunk regardless of provider; the method's msgType != "user" early-return safely no-ops on Codex data.
  • Codex.FindSessionByID returns the ROOT thread for any partial UUID matching a subagent. The package-private findRolloutByID helper resolves concrete rollout files before the walk-up step.
  • DetectInstalled() returns names in fixed detectOrder (claude-code first, then codex) regardless of LookPath lookup order. This determinism is load-bearing for setup output and tests.
  • CLIBinaryName() is the OS binary name ("claude", "codex") — never the canonical provider name. The two diverge for Claude Code (claude-code vs claude).
  • Codex.InstallHooks installs SessionStart, PreToolUse, and PostToolUse. Daemon shutdown is driven by parent-PID liveness, never by Codex Stop.
  • Codex.InstallSkills writes only SKILL.md files under ~/.codex/skills/<name>/; optional Codex UI metadata such as agents/openai.yaml is not generated for Confab's bundled skills.
  • CodexRolloutMetadata JSON tags are wire-format pins. Existing rows in the backend's codex_rollouts table were written against these tags; renaming any field is a backwards-incompatible change. Adding new optional fields (with omitempty) is safe.
  • CodexRolloutMetadata string fields (cwd, model, agent_*) ride on the first chunk unredacted. Rollout content is redacted in pkg/sync.FileTracker.ReadChunk; this struct is not. Before adding a field that could carry free-text user content, plumb the redactor into Codex.InitTranscript / Codex.DiscoverDescendants — see the struct doc in codex_rollout.go.
  • Sync-loop providers (InitTranscript, DiscoverDescendants, DiscoverWorkflowFiles, AnnotateChunk) are called from a single goroutine inside the engine's sync loop. Implementations may mutate the passed TranscriptRegistrar / DescendantRegistrar / WorkflowRegistrar / ChunkView without locking; the engine does not call them concurrently for the same engine instance.

Used By

cmd/, pkg/hookconfig/ (provider provides the file paths; hookconfig does the file editing), pkg/sync/ (the engine dispatches root metadata, descendant discovery, and chunk annotation through the Provider interface; the tracker calls ClaudeCode{}.ExtractAgentIDsFromMessage directly for Claude sidechain discovery).

Documentation

Index

Constants

View Source
const (
	NameClaudeCode = "claude-code"
	NameCodex      = "codex"
)
View Source
const ClaudeStateDirEnv = "CONFAB_CLAUDE_DIR"

ClaudeStateDirEnv is the environment variable to override the default Claude state directory.

View Source
const CodexStateDBEnv = "CONFAB_CODEX_STATE_DB"

CodexStateDBEnv overrides automatic state DB discovery. When set, points directly at a state_N.sqlite file (or any SQLite file with the expected schema). Used by tests; can also be set by power users debugging Codex state inconsistencies.

View Source
const CodexStateDirEnv = "CONFAB_CODEX_DIR"
View Source
const FileTypeWorkflowJournal = "workflow_journal"

FileTypeWorkflowJournal is the sync file_type for a Claude workflow run's journal.jsonl (CF-533). The backend (CF-532) accepts and stores it but never Claude-parses it; it is excluded from token/transcript analytics. Workflow subagent transcripts use the ordinary "agent" file_type.

Variables

View Source
var LookPath = exec.LookPath

LookPath is the package-level seam tests stub to simulate CLI presence.

Functions

func DetectInstalled

func DetectInstalled() []string

DetectInstalled returns the canonical names of providers whose CLI binary is on PATH, in fixed registry order. Result is never nil but may be empty.

func NormalizeName

func NormalizeName(name string) (string, error)

NormalizeName returns the canonical provider name. Backed by the registry so it can't drift from the Provider list.

func ResetStateDBPathCacheForTest

func ResetStateDBPathCacheForTest()

ResetStateDBPathCacheForTest clears the cached state DB path so the next StateDBPath call re-evaluates the env var and glob. Production code resolves the path once per process; tests need to swap fixtures between cases. Lives in non-test code so cross-package tests (sync, daemon, cmd) can call it.

func ResetWalkUpRetryForTest

func ResetWalkUpRetryForTest()

ResetWalkUpRetryForTest restores production retry defaults.

func SetWalkUpRetryForTest

func SetWalkUpRetryForTest(attempts int, backoff time.Duration)

SetWalkUpRetryForTest shrinks WalkUpToRoot's retry budget for tests that would otherwise wait for the full production timeout. Pair with ResetWalkUpRetryForTest in t.Cleanup.

Types

type AnnotationResult

type AnnotationResult struct {
	IncludedFirstUserMessage bool
	SummaryLinks             []SummaryLink
}

AnnotationResult is the structured return from AnnotateChunk. IncludedFirstUserMessage tells the engine whether to flip its sentFirstUserMessage flag. SummaryLinks (Claude only) tells the engine which parent summaries to link via the backend.

type ChunkView

type ChunkView interface {
	FileType() string
	FirstLine() int
	Lines() []string
	FileCodexRollout() *CodexRolloutMetadata
	SetCodexRolloutMetadata(*CodexRolloutMetadata)
	SetSummary(string)
	SetFirstUserMessage(string)
}

ChunkView is the structural view of a chunk + its source file that AnnotateChunk reads from and writes back into. pkg/sync's chunkView adapter satisfies it. Setters mutate the underlying chunk's metadata in place; accessors return read-only snapshots.

type ClaudeCode

type ClaudeCode struct{}

ClaudeCode contains Claude Code-specific local behavior.

func (ClaudeCode) AnnotateChunk

func (p ClaudeCode) AnnotateChunk(c ChunkView, _ bool, redact func(string) string) AnnotationResult

AnnotateChunk extracts the local summary, first user message, and summary-link records from a Claude Code transcript chunk. Summary links are returned via AnnotationResult.SummaryLinks so the engine can perform the backend HTTP after AnnotateChunk returns — keeping the provider side-effect-free.

Non-transcript files are a no-op (Claude extracts only from transcripts).

Claude does not gate first-user-message extraction on a "first time" flag — the discovery helper handles dedup internally and the engine historically does not flip sentFirstUserMessage for Claude. The returned IncludedFirstUserMessage stays false so the engine's flag is untouched.

func (ClaudeCode) CLIBinaryName

func (ClaudeCode) CLIBinaryName() string

CLIBinaryName returns "claude" — the binary `claude` users install via the Claude Code installer.

func (ClaudeCode) DefaultCWD

func (p ClaudeCode) DefaultCWD(transcriptPath string) string

DefaultCWD returns filepath.Dir(transcriptPath); Claude has no richer per-session CWD source.

func (ClaudeCode) DiscoverDescendants

func (ClaudeCode) DiscoverDescendants(DescendantRegistrar, string) error

DiscoverDescendants is a no-op for Claude Code. Claude's agent files are discovered transitively from transcript content (agent IDs embedded in JSONL messages) inside tracker.DiscoverNewFiles — no external state DB lookup is required.

func (ClaudeCode) DiscoverWorkflowFiles added in v0.16.2

func (ClaudeCode) DiscoverWorkflowFiles(reg WorkflowRegistrar, allow func(fileType string) bool) (int, error)

DiscoverWorkflowFiles implements provider.Provider — see the interface doc.

func (ClaudeCode) ExtractAgentIDsFromMessage

func (ClaudeCode) ExtractAgentIDsFromMessage(message map[string]interface{}) []string

ExtractAgentIDsFromMessage extracts agent IDs from a parsed Claude transcript message. Checks both root-level toolUseResult.agentId and nested content blocks. Empty slice on non-user messages or missing fields.

func (ClaudeCode) ExtractMetadata

func (p ClaudeCode) ExtractMetadata(lines []string) SessionMetadata

ExtractMetadata parses summary, first user message, and summary links from in-memory Claude transcript lines. Lines beyond maxLinesForExtraction are ignored.

For summaries:

  • Entries with a leafUuid go to SummaryLinks (links to previous sessions).
  • Entries without leafUuid become the local Summary (last one wins).

For user messages: the first one encountered sets FirstUserMessage.

func (ClaudeCode) FindParentPID

func (p ClaudeCode) FindParentPID() int

FindParentPID walks up the process tree to find the Claude Code process.

func (ClaudeCode) FindSessionByID

func (p ClaudeCode) FindSessionByID(partialID string) (string, string, error)

FindSessionByID resolves a full or partial Claude session ID to its full ID and transcript path. Walk-up is identity for Claude (no thread tree). Returns an error on no-match or ambiguous prefix.

func (ClaudeCode) InitTranscript

func (ClaudeCode) InitTranscript(TranscriptRegistrar, string, string) error

InitTranscript is a no-op for Claude Code — there is no root rollout metadata to attach (Codex-only concern).

func (ClaudeCode) InstallHooks

func (p ClaudeCode) InstallHooks() (string, error)

InstallHooks installs all four Confab hook bundles (sync, PreToolUse, PostToolUse, UserPromptSubmit). Returns the settings.json path.

func (ClaudeCode) InstallSkills

func (p ClaudeCode) InstallSkills() error

InstallSkills installs the Claude Code skills shipped with confab (/retro) and prunes any retired skills left by older versions.

func (ClaudeCode) IsHooksInstalled

func (ClaudeCode) IsHooksInstalled() (bool, error)

IsHooksInstalled reports whether all four Confab hook bundles for Claude Code are installed. Mirrors InstallHooks: true only when every bundle is present.

func (ClaudeCode) IsProcess

func (p ClaudeCode) IsProcess(pid int) bool

IsProcess checks if the given PID is a Claude Code process.

func (ClaudeCode) IsSkillInstalled

func (p ClaudeCode) IsSkillInstalled(name string) bool

IsSkillInstalled reports whether a shipped Claude Code skill exists.

func (ClaudeCode) MatchesProcess

func (ClaudeCode) MatchesProcess(cmd string) bool

MatchesProcess checks if a command string matches Claude Code.

func (ClaudeCode) Name

func (ClaudeCode) Name() string

Name returns the canonical Claude Code provider name.

func (ClaudeCode) ParseSessionHook

func (p ClaudeCode) ParseSessionHook(r io.Reader) (HookInput, error)

ParseSessionHook reads a Claude SessionStart hook payload and returns the provider-agnostic view.

func (ClaudeCode) ProjectsDir

func (p ClaudeCode) ProjectsDir() (string, error)

ProjectsDir returns the Claude projects directory.

func (ClaudeCode) ReadHookInput

func (ClaudeCode) ReadHookInput(r io.Reader) (*types.ClaudeHookInput, error)

ReadHookInput reads and validates Claude hook JSON.

func (ClaudeCode) ReadSessionHookInput

func (p ClaudeCode) ReadSessionHookInput(r io.Reader) (*types.ClaudeHookInput, error)

ReadSessionHookInput reads Claude session hook JSON and validates transcript_path.

func (ClaudeCode) ScanSessions

func (p ClaudeCode) ScanSessions() ([]SessionInfo, error)

ScanSessions walks ~/.claude/projects/ and returns all user sessions sorted oldest first. Permission errors per path are reported to stderr and don't fail the scan.

func (ClaudeCode) SettingsPath

func (p ClaudeCode) SettingsPath() (string, error)

SettingsPath returns the Claude settings file path.

func (ClaudeCode) ShouldSpawnForInput

func (ClaudeCode) ShouldSpawnForInput(HookInput) bool

ShouldSpawnForInput is unconditional for Claude Code.

func (ClaudeCode) StateDir

func (ClaudeCode) StateDir() (string, error)

StateDir returns the Claude state directory. Defaults to ~/.claude but can be overridden with CONFAB_CLAUDE_DIR.

func (ClaudeCode) SupportsCommitLinking added in v0.16.1

func (ClaudeCode) SupportsCommitLinking() bool

SupportsCommitLinking reports that Claude Code installs PreToolUse + PostToolUse hooks that drive bidirectional GitHub linking.

func (ClaudeCode) UninstallHooks

func (p ClaudeCode) UninstallHooks() (string, error)

UninstallHooks removes all four Confab hook bundles. Returns the settings.json path even if no hooks were present.

func (ClaudeCode) UninstallSkills

func (p ClaudeCode) UninstallSkills() error

UninstallSkills removes the Claude Code skills shipped with confab.

func (ClaudeCode) ValidateTranscriptPath

func (p ClaudeCode) ValidateTranscriptPath(path string) error

ValidateTranscriptPath checks that a Claude transcript path is safe: - Must be absolute - Must not contain ".." components - Must resolve to a location under the Claude projects directory

func (ClaudeCode) WalkUpToRoot

func (ClaudeCode) WalkUpToRoot(sessionID string) (string, string, error)

WalkUpToRoot is the identity walk for Claude Code: there is no thread tree, so the firing session is always its own root and rootPath is "".

func (ClaudeCode) WriteHookResponse

func (ClaudeCode) WriteHookResponse(w io.Writer, suppressOutput bool, systemMessage string) error

WriteHookResponse writes a ClaudeHookResponse to w.

type Codex

type Codex struct{}

func (Codex) AnnotateChunk

func (p Codex) AnnotateChunk(c ChunkView, sentFirstUserMessage bool, redact func(string) string) AnnotationResult

AnnotateChunk attaches Codex-specific chunk metadata. Two concerns are handled independently:

  • first_user_message: extracted via ExtractMetadata from the root transcript's chunks (Codex emits the user prompt once at the start). Gated by c.FileType() == "transcript" + !sentFirstUserMessage. The closure `redact` (nil-safe) is applied before attaching.

  • codex_rollout: per-rollout metadata so the backend can upsert the `codex_rollouts` row. Emitted on the FIRST chunk of any Codex rollout (root or descendant) — detected via c.FirstLine() == 1. No persistent state flag is needed; retries preserve FirstLine == 1. Backend upsert is idempotent.

func (Codex) CLIBinaryName

func (Codex) CLIBinaryName() string

CLIBinaryName returns "codex" — the binary users install via Codex.

func (Codex) ConfigPath

func (p Codex) ConfigPath() (string, error)

func (Codex) DefaultCWD

func (p Codex) DefaultCWD(transcriptPath string) string

DefaultCWD reads session_meta.cwd from the rollout. Falls back to filepath.Dir on read failure or empty CWD so the upload still has a directory to record.

func (Codex) DiscoverDescendants

func (p Codex) DiscoverDescendants(reg DescendantRegistrar, rootThreadUUID string) error

DiscoverDescendants queries the local Codex SQLite state DB for every descendant of rootThreadUUID, verifies each rollout file exists and looks like an actual subagent (ValidateRolloutPath + !IsUserSession check on session_meta), and registers verified ones via reg.RegisterCodexRollout.

Idempotent across calls (skips already-tracked filenames). Degrades gracefully when the state DB is missing or its schema doesn't match — ListSubtree returns (nil, nil) and we return nil. Per-descendant verification failures log at warn level and skip the row.

func (Codex) DiscoverWorkflowFiles added in v0.16.2

func (Codex) DiscoverWorkflowFiles(WorkflowRegistrar, func(fileType string) bool) (int, error)

DiscoverWorkflowFiles is a no-op for Codex: workflow subagents are a Claude Code harness feature (the Workflow tool) with no Codex equivalent, so there are never workflow run dirs to scan. The allow predicate is intentionally never invoked, so a Codex session never triggers a backend capability probe.

func (Codex) ExtractFirstUserMessageFromLines

func (Codex) ExtractFirstUserMessageFromLines(lines []string) string

ExtractFirstUserMessageFromLines returns the first non-empty user message found in the given rollout lines, truncated to MaxFirstUserMessageLength bytes on a UTF-8 boundary. Returns "" when no user message is present.

func (Codex) ExtractMetadata

func (p Codex) ExtractMetadata(lines []string) SessionMetadata

ExtractMetadata returns the in-memory metadata for a Codex chunk. Summary and SummaryLinks stay empty — those are Claude-only concepts. Lines are capped to maxLinesForExtraction to mirror Claude's bound.

func (Codex) FindParentPID

func (p Codex) FindParentPID() int

FindParentPID walks up the process tree to find the Codex process. Mirrors ClaudeCode.FindParentPID for daemon parent-liveness monitoring.

func (Codex) FindSessionByID

func (p Codex) FindSessionByID(partialID string) (string, string, error)

FindSessionByID resolves a full or partial UUID to the ROOT thread's UUID and rollout path. If partialID matches a subagent, this walks up to the top-most user session via WalkUpToRoot so callers transparently upload the whole tree.

func (Codex) InitTranscript

func (p Codex) InitTranscript(target TranscriptRegistrar, transcriptPath, externalID string) error

InitTranscript reads the root rollout's session_meta and attaches the resulting CodexRolloutMetadata to the transcript file so the very first uploaded chunk carries codex_rollout metadata. On read failure (missing or malformed rollout file) logs at warn level and still attaches the bare-minimum metadata (ThreadUUID + RolloutPath) so the backend can upsert a row; CWD/Model/etc. stay empty. Matches pre-CF-397 behavior. Never returns an error — failure modes are recoverable.

func (Codex) InstallHooks

func (p Codex) InstallHooks() (string, error)

InstallHooks delegates to pkg/hookconfig.

func (Codex) InstallSkills

func (p Codex) InstallSkills() error

InstallSkills installs the Codex skills shipped with confab (/retro) and prunes any retired skills left by older versions.

func (Codex) IsHooksInstalled

func (p Codex) IsHooksInstalled() (bool, error)

IsHooksInstalled delegates to pkg/hookconfig, which parses ~/.codex/config.toml and returns true iff a confab command is registered under [[hooks.SessionStart]].

func (Codex) IsProcess

func (p Codex) IsProcess(pid int) bool

IsProcess checks if the given PID is a Codex process.

func (Codex) IsSkillInstalled

func (p Codex) IsSkillInstalled(name string) bool

IsSkillInstalled reports whether a shipped Codex skill exists.

func (Codex) ListSubtree

func (p Codex) ListSubtree(rootThreadUUID string) ([]CodexThreadRow, error)

ListSubtree returns every descendant of rootThreadUUID, at any depth, via a recursive CTE over `thread_spawn_edges` JOIN `threads`. Each returned row's ParentThreadUUID is the immediate parent (which may itself be a descendant of rootThreadUUID for grandchildren). The root itself is NOT returned.

Returns (nil, nil) when the DB doesn't exist or the schema doesn't match — degrades gracefully so daemon sync cycles continue uninterrupted.

func (Codex) MatchesProcess

func (Codex) MatchesProcess(cmd string) bool

MatchesProcess checks if a command string matches a Codex invocation.

func (Codex) Name

func (Codex) Name() string

Name returns the canonical Codex provider name.

func (Codex) ParseSessionHook

func (p Codex) ParseSessionHook(r io.Reader) (HookInput, error)

ParseSessionHook reads a Codex SessionStart hook payload and returns the provider-agnostic view.

func (Codex) ReadHookInput

func (p Codex) ReadHookInput(r io.Reader) (*types.CodexHookInput, error)

func (Codex) ReadSessionHookInput

func (p Codex) ReadSessionHookInput(r io.Reader) (*types.CodexHookInput, error)

func (Codex) ReadSessionInfo

func (p Codex) ReadSessionInfo(path string) (CodexSessionInfo, error)

ReadSessionInfo reads a rollout's session_meta line and returns the parsed CodexSessionInfo (with file stat info populated).

func (Codex) ScanCodexSessions

func (p Codex) ScanCodexSessions() ([]CodexSessionInfo, error)

ScanCodexSessions returns the rich Codex-specific session info for every user-initiated rollout. Internal callers that need CodexSessionInfo's extras (CWD, Model, AgentRole, ...) use this directly; the cross-provider Provider.ScanSessions interface method projects to []SessionInfo.

func (Codex) ScanSessions

func (p Codex) ScanSessions() ([]SessionInfo, error)

ScanSessions projects ScanCodexSessions to the cross-provider SessionInfo shape. FirstUserMessage is extracted from each rollout's first event_msg.user_message line (capped to maxLinesForExtraction). Sessions are returned oldest first to match Claude's ordering.

func (Codex) SessionIDFromRolloutPath

func (Codex) SessionIDFromRolloutPath(path string) (string, bool)

SessionIDFromRolloutPath parses the UUID embedded in a Codex rollout filename. Returns ("", false) on filenames that don't match the rollout pattern.

func (Codex) SessionsDir

func (p Codex) SessionsDir() (string, error)

func (Codex) ShouldSpawnForInput

func (p Codex) ShouldSpawnForInput(in HookInput) bool

ShouldSpawnForInput inspects the rollout file's session_meta to decide whether the firing hook represents a user-initiated rollout (true) or a subagent (false).

func (Codex) StateDBPath

func (p Codex) StateDBPath() (string, error)

StateDBPath resolves the path to Codex's local state SQLite DB. Resolution order:

  1. CONFAB_CODEX_STATE_DB env var (escape hatch for tests/debugging).
  2. Glob `<StateDir>/state_*.sqlite`, parse the integer suffix between `_` and `.sqlite`, return the entry with the highest numeric suffix. If suffixes don't parse as integers, falls back to alphabetical max.
  3. Returns os.ErrNotExist if no candidate file exists.

The result is cached for the lifetime of the process via sync.Once.

func (Codex) StateDir

func (Codex) StateDir() (string, error)

func (Codex) SupportsCommitLinking added in v0.16.1

func (Codex) SupportsCommitLinking() bool

SupportsCommitLinking reports that Codex installs PreToolUse + PostToolUse hooks that drive bidirectional GitHub linking. See pkg/hookconfig/codex.go for the managed-block contents.

func (Codex) UninstallHooks

func (p Codex) UninstallHooks() (string, error)

UninstallHooks delegates to pkg/hookconfig.

func (Codex) UninstallSkills

func (p Codex) UninstallSkills() error

UninstallSkills removes the Codex skills shipped with confab.

func (Codex) ValidateRolloutPath

func (p Codex) ValidateRolloutPath(path string) error

func (Codex) WalkUpToRoot

func (p Codex) WalkUpToRoot(threadUUID string) (rootUUID, rootRolloutPath string, err error)

WalkUpToRoot walks the parent_thread_id chain in `thread_spawn_edges` starting from threadUUID until it reaches a thread with no parent edge. Returns the top-most root's UUID + that root's rollout_path.

If threadUUID is already a root (no parent edge), returns it unchanged. If the DB is unavailable or the thread isn't in the DB at all, returns (threadUUID, "", nil) — graceful degradation so callers don't need to branch on transient errors.

Built-in retry: the FIRST parent lookup retries up to walkUpRetryAttempts times with walkUpRetryBackoff between attempts. This absorbs the spawn-vs-edge race where Codex fires the SessionStart hook for a fresh subagent before the `thread_spawn_edges` row has been committed.

The retry only fires on the first hop; subsequent hops (grandchild → child → root walks) assume the chain is already stable in Codex's DB.

Logs the retry-attempt count + wall time at info level so we can later tune the retry budget against observed race timing.

func (Codex) WriteHookResponse

func (Codex) WriteHookResponse(w io.Writer, suppressOutput bool, systemMessage string) error

WriteHookResponse writes a CodexHookResponse to w.

type CodexRolloutMetadata

type CodexRolloutMetadata struct {
	ThreadUUID       string `json:"thread_uuid"`
	ParentThreadUUID string `json:"parent_thread_uuid,omitempty"` // "" for roots
	RolloutPath      string `json:"rollout_path"`
	CWD              string `json:"cwd,omitempty"`
	Model            string `json:"model,omitempty"`
	// Source is the flattened discriminator from Codex's polymorphic
	// session_meta.source field — a short string like "cli" or "subagent".
	// The backend's `codex_rollouts.source` column caps this at 64 chars.
	Source        string `json:"source,omitempty"`
	ThreadSource  string `json:"thread_source,omitempty"`
	AgentPath     string `json:"agent_path,omitempty"`
	AgentRole     string `json:"agent_role,omitempty"`
	AgentNickname string `json:"agent_nickname,omitempty"`
}

CodexRolloutMetadata is the per-rollout metadata transmitted on the FIRST chunk of a Codex rollout (root or descendant). The backend upserts it into the `codex_rollouts` table keyed by ThreadUUID. Omitted on chunks where chunk.FirstLine != 1, so the backend handler treats absence as "no metadata to record this round."

Codex-only; the backend rejects this field on non-codex sessions with 400.

Lives in pkg/provider so both pkg/sync (wire format) and pkg/provider's Codex implementation can construct one without a package import cycle. pkg/sync re-exports it via `type CodexRolloutMetadata = provider.CodexRolloutMetadata`.

Redaction: fields below are sourced from Codex's session_meta (and the SQLite state DB for descendants) and ride on the first chunk unredacted. Rollout *content* is redacted in pkg/sync.FileTracker.ReadChunk; these metadata fields are not. Current fields are short, structured values (path, model name, agent role). Before adding a field that could carry free-text user content, plumb the redactor into Codex.InitTranscript / Codex.DiscoverDescendants rather than extending this struct.

type CodexSessionInfo

type CodexSessionInfo struct {
	SessionID   string
	RolloutPath string
	CWD         string
	Model       string
	// Source is a short discriminator extracted from the rollout's `source`
	// field. Codex writes that field as either a bare string ("cli") for
	// user-initiated rollouts or a tagged object ({"subagent":{...}}) for
	// spawned subagents. The string case is passed through; the object case
	// is collapsed to its top-level key. Empty when session_meta omits the
	// field. Matches the backend's 64-char `codex_rollouts.source` column.
	Source        string
	ThreadSource  string
	AgentPath     string
	AgentRole     string
	AgentNickname string
	ModTime       time.Time
	SizeBytes     int64
}

CodexSessionInfo is the rich Codex-specific session metadata returned by ScanCodexSessions and ReadSessionInfo. Internal callers (cmd/save.go, engine init paths) need the extras (CWD, Model, AgentRole, ...) that don't fit on the cross-provider SessionInfo.

func (CodexSessionInfo) IsUserSession

func (s CodexSessionInfo) IsUserSession() bool

IsUserSession reports whether this rollout is a top-level user session (not a subagent or memory rollout).

type CodexThreadRow

type CodexThreadRow struct {
	ThreadUUID       string
	ParentThreadUUID string
	RolloutPath      string
	CWD              string
	Model            string
	Source           string
	ThreadSource     string
	AgentPath        string
	AgentRole        string
	AgentNickname    string
}

CodexThreadRow describes a single Codex thread as recorded in the local state DB's `threads` table. ParentThreadUUID is the immediate parent recorded in `thread_spawn_edges`, or "" for a root thread.

type DescendantRegistrar

type DescendantRegistrar interface {
	IsTracked(fileName string) bool
	RegisterCodexRollout(path, fileName string, isRoot bool, meta CodexRolloutMetadata)
}

DescendantRegistrar is the surface DiscoverDescendants uses to register newly-discovered sidechain files. *sync.FileTracker satisfies it via IsTracked + RegisterCodexRollout.

type HookInput

type HookInput interface {
	SessionID() string
	TranscriptPath() string
	CWD() string
	HookEventName() string
	ParentPID() int
}

HookInput is the provider-agnostic view of a hook payload, exposing the fields used by daemon spawning and bookkeeping. Concrete shapes (types.ClaudeHookInput, types.CodexHookInput) satisfy this via adapter types defined in hookinput.go.

type Provider

type Provider interface {
	Name() string
	// CLIBinaryName is the OS-level binary name to look up via
	// exec.LookPath when detecting whether the provider is installed
	// locally (e.g. "claude" for Claude Code, "codex" for Codex).
	CLIBinaryName() string
	StateDir() (string, error)
	FindParentPID() int
	IsProcess(pid int) bool

	// SupportsCommitLinking reports whether this provider's hook system
	// can drive bidirectional GitHub linking (commit-trailer + PR-body
	// injection via PreToolUse; commit/PR URL linking via PostToolUse).
	// Used by cmd/ handlers to silently no-op for providers that don't
	// install those events.
	SupportsCommitLinking() bool

	// ParseSessionHook reads a SessionStart-style hook payload from r and
	// returns the provider-agnostic view.
	ParseSessionHook(r io.Reader) (HookInput, error)

	InstallHooks() (string, error)
	UninstallHooks() (string, error)
	IsHooksInstalled() (bool, error)

	// InstallSkills installs the bundled skills for this provider's local
	// skill layout.
	InstallSkills() error
	UninstallSkills() error
	IsSkillInstalled(name string) bool

	// WalkUpToRoot returns the root session ID and its rollout path. For
	// providers without a separate root file identifier (Claude Code),
	// rootPath is "".
	WalkUpToRoot(sessionID string) (rootID, rootPath string, err error)

	// ShouldSpawnForInput is the per-provider gate on whether a fresh
	// SessionStart should result in a daemon. Codex returns false for
	// subagent rollouts; Claude is always true.
	ShouldSpawnForInput(in HookInput) bool

	// WriteHookResponse writes a hook response payload to w. The response
	// shape is provider-specific but the (continue, suppressOutput,
	// systemMessage) tuple is shared.
	WriteHookResponse(w io.Writer, suppressOutput bool, systemMessage string) error

	// InitTranscript is called from sync.Engine.Init AFTER the backend has
	// returned the initial sync state and the transcript file has been
	// registered in the tracker. Codex reads session_meta and attaches
	// codex_rollout metadata to the root transcript so the first chunk
	// uploaded carries it. Claude is a no-op. The engine logs+continues on
	// error; implementations may return one for true I/O failures.
	InitTranscript(target TranscriptRegistrar, transcriptPath, externalID string) error

	// DiscoverDescendants is called once per SyncAll cycle, BEFORE the BFS
	// loop. Providers with an external discovery model (Codex: SQLite
	// subtree walk) register newly-discovered sidechain files via reg.
	// Must be idempotent across calls (skip already-tracked filenames).
	// Claude is a no-op (its agents are discovered transitively from
	// transcript content, handled in tracker.DiscoverNewFiles).
	DiscoverDescendants(reg DescendantRegistrar, externalID string) error

	// DiscoverWorkflowFiles is called once per SyncAll cycle, alongside
	// DiscoverDescendants, to register Claude workflow subagent transcripts
	// (subagents/workflows/<runId>/agent-<id>.jsonl) and run journals
	// (.../journal.jsonl) as path-encoded sidechain files (CF-533). The
	// allow predicate is supplied by the engine and gates each file by its
	// file_type against backend capability ("agent" → workflow_files,
	// "workflow_journal" → workflow_journal); the provider calls it only
	// after it has a candidate file, so non-workflow sessions never trigger
	// a backend capability probe. Returns the count of newly-registered
	// files (for logging). Claude scans the filesystem; Codex is a no-op
	// (no Workflow-tool equivalent). Must be idempotent across calls.
	DiscoverWorkflowFiles(reg WorkflowRegistrar, allow func(fileType string) bool) (int, error)

	// AnnotateChunk is called for every chunk read from a tracked file,
	// BEFORE upload. Providers attach provider-specific chunk metadata
	// (codex_rollout, first_user_message, summary). The redact closure is
	// nil-safe; providers must guard `if redact != nil { ... }` before
	// applying. Engine inspects the returned AnnotationResult to flip its
	// sentFirstUserMessage flag and dispatch any extracted summary links.
	AnnotateChunk(c ChunkView, sentFirstUserMessage bool, redact func(string) string) AnnotationResult

	// ScanSessions returns the user-initiated sessions discoverable on
	// disk for this provider, sorted oldest first. Subagent rollouts and
	// other non-user transcripts are filtered out.
	ScanSessions() ([]SessionInfo, error)

	// FindSessionByID resolves a full or partial session ID to its full
	// ID and transcript path. For providers with a thread tree (Codex)
	// this walks up to the root so the returned (id, path) refer to the
	// top-most user session — callers that want the unwalked rollout use
	// provider-specific methods.
	FindSessionByID(partialID string) (id, transcriptPath string, err error)

	// ExtractMetadata parses summary, first user message, and
	// (Claude-only) summary links from in-memory transcript lines.
	// Implementations cap the line count to bound cost.
	ExtractMetadata(lines []string) SessionMetadata

	// DefaultCWD returns the working directory to record alongside an
	// upload for this transcript path. Claude derives from the path;
	// Codex reads session_meta.cwd with a path-dir fallback.
	DefaultCWD(transcriptPath string) string
}

Provider abstracts per-tool local behavior. Adding a new provider means implementing this interface and registering the instance.

func Get

func Get(name string) (Provider, error)

Get returns the registered Provider for name. An empty string resolves to Claude Code for backwards compatibility with NormalizeName.

type SessionInfo

type SessionInfo struct {
	SessionID        string
	TranscriptPath   string
	ProjectPath      string
	ModTime          time.Time
	SizeBytes        int64
	Summary          string
	FirstUserMessage string
}

SessionInfo is the cross-provider shape returned by Provider.ScanSessions and Provider.FindSessionByID. Concrete provider types may keep richer internal forms (e.g. CodexSessionInfo) and project to SessionInfo at the seams.

type SessionMetadata

type SessionMetadata struct {
	Summary          string
	FirstUserMessage string
	SummaryLinks     []SummaryLink
}

SessionMetadata is the parsed metadata for a transcript file or in-memory chunk. SummaryLinks are Claude-only and stay nil for other providers.

type SummaryLink struct {
	Summary  string
	LeafUUID string
}

SummaryLink describes a parent-session summary link extracted from a Claude transcript chunk. The engine HTTPs them after AnnotateChunk returns; the provider remains side-effect-free.

type TranscriptRegistrar

type TranscriptRegistrar interface {
	SetCodexRollout(*CodexRolloutMetadata)
}

TranscriptRegistrar is the minimal surface InitTranscript sees on the root transcript's TrackedFile. *sync.TrackedFile satisfies it structurally via SetCodexRollout. Lives here (not pkg/sync) to avoid an import cycle.

type WorkflowRegistrar added in v0.16.2

type WorkflowRegistrar interface {
	SubagentsDir() string
	RegisterWorkflowFile(path, name, fileType string) bool
}

WorkflowRegistrar is the surface DiscoverWorkflowFiles uses to register Claude workflow subagent transcripts + run journals as path-encoded sidechain files. *sync.FileTracker satisfies it. SubagentsDir exposes the session's <session>/subagents directory (workflow runs live beneath subagents/workflows/<runId>/); RegisterWorkflowFile tracks a file by its backend file_name (forward-slash path-encoded) and reports whether it was newly added (vs. an in-place path/type correction of an existing entry).

Jump to

Keyboard shortcuts

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