Documentation
¶
Overview ¶
Package detection: this file implements the TOML→DTO parsing layer for detector plugins (schema v1, see project_plans/detector-plugins/decisions/ADR-003-plugin-toml-schema-v1.md). Schema v1 has no `priority` key — ordering within a status category is declaration order, matching what PatternSet.MatchLines actually does. The DTOs defined here (pluginFile, patternEntry) are an internal parsing representation only; they never escape the loader and must not be passed to code outside this package.
Index ¶
- Constants
- func DetectedStatusToProto(s DetectedStatus) sessionv1.DetectedStatus
- func DetectedStatusToSubStatus(s DetectedStatus) sessionv1.SubStatus
- func DetectorProvenance() map[string]string
- func EnsurePluginDir() (string, error)
- func FormatDuration(d time.Duration) string
- func HasActiveScreenRedraw(raw []byte) bool
- func HasClaudeSpinnerActivity(tail string) bool
- func InitPlugins(ctx context.Context) error
- func IsOSCExecutingPromotable(status DetectedStatus) bool
- func IsOSCIdlePromotable(status DetectedStatus) bool
- func LoadPluginDir(ctx context.Context, dir string) ([]*PluginDetector, []PluginLoadError)
- func PluginDir() (string, error)
- type ApprovalDetector
- func (ad *ApprovalDetector) AddPattern(pattern *ApprovalPattern) error
- func (ad *ApprovalDetector) ClearHistory()
- func (ad *ApprovalDetector) Detect(output string) []*ApprovalRequest
- func (ad *ApprovalDetector) DetectInChunk(data []byte, err error) *ApprovalRequest
- func (ad *ApprovalDetector) GetHistory(limit int) []*ApprovalRequest
- func (ad *ApprovalDetector) GetMaxHistory() int
- func (ad *ApprovalDetector) GetPatterns() []*ApprovalPattern
- func (ad *ApprovalDetector) GetPendingRequests() []*ApprovalRequest
- func (ad *ApprovalDetector) GetRequestByID(id string) *ApprovalRequest
- func (ad *ApprovalDetector) GetStatistics() ApprovalStatistics
- func (ad *ApprovalDetector) RemovePattern(name string) bool
- func (ad *ApprovalDetector) SetMaxHistory(max int)
- func (ad *ApprovalDetector) Subscribe(subscriberID string) <-chan *ApprovalRequest
- func (ad *ApprovalDetector) Unsubscribe(subscriberID string)
- func (ad *ApprovalDetector) UpdateRequestStatus(id string, status ApprovalRequestStatus, response *ApprovalResponse) error
- type ApprovalPattern
- type ApprovalRequest
- type ApprovalRequestStatus
- type ApprovalResponse
- type ApprovalStatistics
- type ApprovalType
- type BinaryDetector
- type DetectedStatus
- type DetectionEvent
- type DetectionEventSink
- type DetectorRegistry
- type IdleDetector
- func NewIdleDetector(sessionName string, ptyAccess PTYReader) *IdleDetector
- func NewIdleDetectorWithConfig(sessionName string, ptyAccess PTYReader, config IdleDetectorConfig) *IdleDetector
- func NewIdleDetectorWithDetector(sessionName string, ptyAccess PTYReader, config IdleDetectorConfig, ...) *IdleDetector
- func (id *IdleDetector) DetectState() IdleState
- func (id *IdleDetector) DetectStateFromContent(content string) IdleState
- func (id *IdleDetector) DetectStateFromContentWithOSC(content string, osc dtypes.OSCStatus) IdleState
- func (id *IdleDetector) GetIdleDuration() time.Duration
- func (id *IdleDetector) GetLastActivity() time.Time
- func (id *IdleDetector) GetLastActivityNs() int64
- func (id *IdleDetector) GetState() IdleState
- func (id *IdleDetector) GetStateInfo() IdleStateInfo
- func (id *IdleDetector) InitializeFromTimestamp(timestamp time.Time)
- func (id *IdleDetector) RecordActivity()
- func (id *IdleDetector) Reset()
- func (id *IdleDetector) UpdateConfig(config IdleDetectorConfig)
- type IdleDetectorConfig
- type IdleState
- type IdleStateInfo
- type PTYNormalizer
- type PTYReader
- type PatternSet
- type PluginDetector
- type PluginLoadError
- type PluginWatcher
- type StatusDetector
- func (sd *StatusDetector) Detect(output []byte) DetectedStatus
- func (sd *StatusDetector) DetectForProgram(output []byte, program string) DetectedStatus
- func (sd *StatusDetector) DetectFromLines(lines []string) DetectedStatus
- func (sd *StatusDetector) DetectFromString(output string) DetectedStatus
- func (sd *StatusDetector) DetectRecent(output []byte, n int) DetectedStatus
- func (sd *StatusDetector) DetectWithContext(output []byte) (DetectedStatus, string)
- func (sd *StatusDetector) DetectWithContextAndCountFromLines(lines []string) (DetectedStatus, string, int)
- func (sd *StatusDetector) DetectWithContextFromLines(lines []string) (DetectedStatus, string)
- func (sd *StatusDetector) ExportPatterns(path string) error
- func (sd *StatusDetector) GetPatternNames(status DetectedStatus) []string
- func (sd *StatusDetector) HasPattern(status DetectedStatus, name string) bool
- func (sd *StatusDetector) LoadPatterns(path string) error
- func (sd *StatusDetector) RecentEvents(n int) []DetectionEvent
- func (sd *StatusDetector) SetSessionID(id string)
- type StatusPattern
- type StatusPatterns
- type TerminalDetector
Constants ¶
const ( // EventRingCap is the maximum number of DetectionEvents retained per StatusDetector. // Increased from 500: ClaudeController and IdleDetector share one ring; detectFromLines // makes up to 50 appendDetectionEvent calls per status check at 1 Hz, draining a // 500-slot ring in ~5 seconds. 2000 slots = ~33 seconds of headroom at 1 Hz. EventRingCap = 2000 // TailSnippetBytes is the maximum bytes captured in TailSnippet. TailSnippetBytes = 512 )
const StatusDetectionTailBytes = 4096
StatusDetectionTailBytes is the number of bytes scanned from the tail of terminal output when determining session status. Matches DefaultIdleDetectorConfig().BufferSize.
Variables ¶
This section is empty.
Functions ¶
func DetectedStatusToProto ¶ added in v1.35.0
func DetectedStatusToProto(s DetectedStatus) sessionv1.DetectedStatus
DetectedStatusToProto converts the internal detection.DetectedStatus iota to the proto enum sessionv1.DetectedStatus. This is the single authoritative mapping; do not duplicate this logic in adapters or converters.
func DetectedStatusToSubStatus ¶ added in v1.41.0
func DetectedStatusToSubStatus(s DetectedStatus) sessionv1.SubStatus
DetectedStatusToSubStatus converts the internal detection.DetectedStatus iota to the proto enum sessionv1.SubStatus. This is the single authoritative mapping; do not duplicate this logic in adapters or converters — call this function instead.
Callers that need additional context (e.g. gating on session.Status or rate-limit state) should apply that logic around a call to this function rather than reimplementing the DetectedStatus switch itself.
func DetectorProvenance ¶ added in v1.41.0
DetectorProvenance returns a fresh copy of the current snapshot's provenance map (binary name -> source: "" for a built-in, a plugin file path otherwise). The copy is defensive: callers must never be able to mutate the live snapshot's map through the returned value.
func EnsurePluginDir ¶ added in v1.41.0
EnsurePluginDir ensures the detector plugin directory (PluginDir()) exists and, on first run, seeds it with a documented example file (example.toml.sample — see examplePluginFile). Returns the directory path.
A failure to create the directory itself is returned to the caller — the directory is essential, there's nothing to scan or watch without it. A failure to write the seed file is only log.Warn'd and swallowed: the seed file is cosmetic documentation, and treating it as fatal would abort the caller's subsequent scan+watch steps even when the directory itself (and any real, pre-existing plugin files in it) are perfectly usable.
func FormatDuration ¶
FormatDuration formats a duration in a human-readable way.
func HasActiveScreenRedraw ¶ added in v1.35.0
HasActiveScreenRedraw reports whether raw PTY bytes contain evidence that the terminal screen is actively being redrawn: a bare carriage return (not part of \r\n) or a cursor-up sequence (\x1b[NA).
func HasClaudeSpinnerActivity ¶ added in v1.35.0
HasClaudeSpinnerActivity reports whether tail contains Claude Code's active-thinking vocabulary as plaintext. This is more targeted than HasActiveScreenRedraw: cursor- positioning sequences appear in both active and idle sessions (from the tmux status bar), but spinner verbs only appear when Claude Code is actively running its spinner.
Use as fallback when filterTmuxMetadata has discarded all content (filtered_len == 0) or when pattern detection falls through to the Ready catch-all on a single-line tail.
func InitPlugins ¶ added in v1.41.0
InitPlugins bootstraps the user detector plugin directory, loads whatever plugins it currently contains, and starts a watcher that hot-reloads the active detector snapshot on change. It is safe to call more than once — a later call is a no-op via initPluginsOnce — but the intended call site is exactly once, from main.go, right after logging is initialized.
Nothing InitPlugins does is fatal: the STAPLER_SQUAD_DISABLE_DETECTOR_PLUGINS kill switch, an unwritable plugin directory, and a failed watcher start are all logged (where relevant) and swallowed, because a detector plugin problem must never prevent the daemon or web server from starting. The error return exists for API stability/future-proofing; every path today returns nil.
func IsOSCExecutingPromotable ¶ added in v1.49.0
func IsOSCExecutingPromotable(status DetectedStatus) bool
IsOSCExecutingPromotable reports whether a text-pattern-derived DetectedStatus is eligible to be promoted to Executing by an OSC-derived spinner signal. Single source of truth shared by applyOSCStatusOverride and IdleDetector.DetectStateFromContentWithOSC — see architecture-review.md BLOCKER 2. Cases are enumerated exhaustively so a future DetectedStatus constant fails the exhaustive lint check rather than silently defaulting.
func IsOSCIdlePromotable ¶ added in v1.49.0
func IsOSCIdlePromotable(status DetectedStatus) bool
IsOSCIdlePromotable reports whether a text-pattern-derived DetectedStatus is eligible to be promoted to Idle by an OSC-derived ✳ signal. See IsOSCExecutingPromotable's doc comment for why cases are enumerated exhaustively.
func LoadPluginDir ¶ added in v1.41.0
func LoadPluginDir(ctx context.Context, dir string) ([]*PluginDetector, []PluginLoadError)
LoadPluginDir scans dir for *.toml detector plugin files, parses and validates each, and returns the resulting detectors plus a list of per-file/per-pattern rejections. One invalid file is reported and skipped; it never prevents other valid files from loading. A missing directory is not an error — it simply yields no plugins, since most users never create one. Non-.toml entries, subdirectories, and symlinks are skipped without error (symlinks are logged — see ADR-004 on why they are never followed).
ctx is checked once per file in the parse/validate loop (not more finely than that — a single file's compile is already time-bounded by maxPluginCompileTime, see validatePluginFile). Without this, a cancelled context is only observed by the caller (rebuildSnapshot) once, at entry, before this function is even called — worst case that delays a graceful shutdown or the next legitimate reload by up to maxPluginFiles * maxPluginCompileTime (200 * 500ms = 100s). On cancellation, the loop stops immediately and returns a fatal "directory"-field PluginLoadError wrapping ctx.Err(), the same shape a directory-read failure produces — the caller's existing fatal-error handling (rebuildSnapshot) already treats that as "leave the previously published snapshot live" (ADR-002), which is exactly what a shutdown-in-progress rebuild should do.
func PluginDir ¶ added in v1.41.0
PluginDir returns the directory scanned for user detector plugin files: config.GetConfigDir()/detectors. This goes through config.GetConfigDir() rather than os.UserHomeDir() directly so plugin scanning honors the same STAPLER_SQUAD_TEST_DIR / STAPLER_SQUAD_INSTANCE isolation as every other piece of application state (config/config.go) — using the real home directory here would leak plugin state across test runs and named instances instead of respecting workspace isolation.
Types ¶
type ApprovalDetector ¶
type ApprovalDetector struct {
// contains filtered or unexported fields
}
ApprovalDetector detects approval requests in command output.
func NewApprovalDetector ¶
func NewApprovalDetector() *ApprovalDetector
NewApprovalDetector creates a new approval detector with default patterns.
func (*ApprovalDetector) AddPattern ¶
func (ad *ApprovalDetector) AddPattern(pattern *ApprovalPattern) error
AddPattern adds a new approval detection pattern.
func (*ApprovalDetector) ClearHistory ¶
func (ad *ApprovalDetector) ClearHistory()
ClearHistory removes all approval detection history.
func (*ApprovalDetector) Detect ¶
func (ad *ApprovalDetector) Detect(output string) []*ApprovalRequest
Detect scans output for approval requests.
func (*ApprovalDetector) DetectInChunk ¶
func (ad *ApprovalDetector) DetectInChunk(data []byte, err error) *ApprovalRequest
DetectInChunk processes a single response chunk for approval patterns.
func (*ApprovalDetector) GetHistory ¶
func (ad *ApprovalDetector) GetHistory(limit int) []*ApprovalRequest
GetHistory returns recent approval detection history.
func (*ApprovalDetector) GetMaxHistory ¶
func (ad *ApprovalDetector) GetMaxHistory() int
GetMaxHistory returns the current max history setting.
func (*ApprovalDetector) GetPatterns ¶
func (ad *ApprovalDetector) GetPatterns() []*ApprovalPattern
GetPatterns returns all registered patterns.
func (*ApprovalDetector) GetPendingRequests ¶
func (ad *ApprovalDetector) GetPendingRequests() []*ApprovalRequest
GetPendingRequests returns all pending approval requests.
func (*ApprovalDetector) GetRequestByID ¶
func (ad *ApprovalDetector) GetRequestByID(id string) *ApprovalRequest
GetRequestByID retrieves a specific approval request by ID.
func (*ApprovalDetector) GetStatistics ¶
func (ad *ApprovalDetector) GetStatistics() ApprovalStatistics
GetStatistics returns statistics about approval detection.
func (*ApprovalDetector) RemovePattern ¶
func (ad *ApprovalDetector) RemovePattern(name string) bool
RemovePattern removes a pattern by name.
func (*ApprovalDetector) SetMaxHistory ¶
func (ad *ApprovalDetector) SetMaxHistory(max int)
SetMaxHistory sets the maximum number of history entries to keep.
func (*ApprovalDetector) Subscribe ¶
func (ad *ApprovalDetector) Subscribe(subscriberID string) <-chan *ApprovalRequest
Subscribe creates a subscription for approval detection events.
func (*ApprovalDetector) Unsubscribe ¶
func (ad *ApprovalDetector) Unsubscribe(subscriberID string)
Unsubscribe removes a subscription.
func (*ApprovalDetector) UpdateRequestStatus ¶
func (ad *ApprovalDetector) UpdateRequestStatus(id string, status ApprovalRequestStatus, response *ApprovalResponse) error
UpdateRequestStatus updates the status of an approval request.
type ApprovalPattern ¶
type ApprovalPattern struct {
Name string `json:"name"`
Type ApprovalType `json:"type"`
Pattern string `json:"pattern"` // Regex pattern
Confidence float64 `json:"confidence"` // Base confidence score
ContextSize int `json:"context_size"` // Lines of context to capture
CaptureKeys []string `json:"capture_keys"` // Names for regex capture groups
// contains filtered or unexported fields
}
ApprovalPattern defines a pattern for detecting approval requests.
type ApprovalRequest ¶
type ApprovalRequest struct {
ID string `json:"id"`
Type ApprovalType `json:"type"`
Timestamp time.Time `json:"timestamp"`
DetectedText string `json:"detected_text"` // The text that matched the pattern
Context string `json:"context"` // Surrounding context
ExtractedData map[string]string `json:"extracted_data"` // Pattern capture groups
Confidence float64 `json:"confidence"` // 0.0-1.0 confidence score
Status ApprovalRequestStatus `json:"status"`
Response *ApprovalResponse `json:"response,omitempty"`
}
ApprovalRequest represents a detected approval request from Claude.
type ApprovalRequestStatus ¶
type ApprovalRequestStatus string
ApprovalRequestStatus tracks the lifecycle of an approval request.
const ( ApprovalPending ApprovalRequestStatus = "pending" ApprovalApproved ApprovalRequestStatus = "approved" ApprovalRejected ApprovalRequestStatus = "rejected" ApprovalExpired ApprovalRequestStatus = "expired" ApprovalIgnored ApprovalRequestStatus = "ignored" )
type ApprovalResponse ¶
type ApprovalResponse struct {
Approved bool `json:"approved"`
Timestamp time.Time `json:"timestamp"`
UserInput string `json:"user_input,omitempty"` // Optional user comment
}
ApprovalResponse contains the user's response to an approval request.
type ApprovalStatistics ¶
type ApprovalStatistics struct {
TotalDetections int
PendingCount int
ApprovedCount int
RejectedCount int
ExpiredCount int
IgnoredCount int
CommandApprovals int
FileWriteApprovals int
FileReadApprovals int
ToolUseApprovals int
ConfirmationApprovals int
}
ApprovalStatistics provides summary statistics.
type ApprovalType ¶
type ApprovalType string
ApprovalType represents different types of approvals Claude might request.
const ( ApprovalCommand ApprovalType = "command" // Shell command approval ApprovalFileWrite ApprovalType = "file_write" // File write/edit approval ApprovalFileRead ApprovalType = "file_read" // File read approval ApprovalToolUse ApprovalType = "tool_use" // Tool/API usage approval ApprovalConfirmation ApprovalType = "confirmation" // Generic confirmation request ApprovalUnknown ApprovalType = "unknown" // Unrecognized approval pattern )
type BinaryDetector ¶ added in v1.35.0
type BinaryDetector = dtypes.BinaryDetector
BinaryDetector provides per-binary pattern sets and optional content filtering. This is a type alias for dtypes.BinaryDetector.
type DetectedStatus ¶
type DetectedStatus int
Status represents the current status of a Claude instance based on PTY output analysis. This extends the existing Status type in instance.go with additional detection capabilities.
const ( StatusUnknown DetectedStatus = iota StatusReady StatusProcessing StatusNeedsApproval StatusInputRequired // Explicit user input prompts (questions, "enter X:", etc.) StatusError StatusTestsFailing // Tests are failing StatusIdle // Waiting for user input (INSERT mode, command prompt, etc.) StatusExecuting // Actively executing commands (shows "esc to interrupt") StatusSuccess // Task completed successfully StatusWaitingForAgent // Waiting for one or more background agents to finish // StatusCompacting is set when Claude is actively summarizing/compacting older // conversation history (distinct from the "N% until auto-compact" // approaching-threshold indicator, which is StatusExecuting). StatusCompacting )
func (DetectedStatus) String ¶
func (s DetectedStatus) String() string
StatusString converts DetectedStatus to a human-readable string.
type DetectionEvent ¶ added in v1.35.0
type DetectionEvent struct {
SessionID string
Timestamp time.Time
MatchedPattern string // Pattern Name field, or "<none>" if StatusUnknown
MatchedCategory string // "active", "processing", "idle", etc., or "unknown"
ResultStatus DetectedStatus
TailSnippet string // Last TailSnippetBytes of cleaned terminal output
}
DetectionEvent records a single invocation of Detect() or DetectWithContext().
type DetectionEventSink ¶ added in v1.35.0
type DetectionEventSink struct {
// contains filtered or unexported fields
}
DetectionEventSink owns the ring buffer and session ID for detection events. StatusDetector delegates all event recording and retrieval to this component.
func (*DetectionEventSink) Recent ¶ added in v1.35.0
func (s *DetectionEventSink) Recent(n int) []DetectionEvent
Recent returns up to n most-recent DetectionEvents, newest-first.
func (*DetectionEventSink) Record ¶ added in v1.35.0
func (s *DetectionEventSink) Record(status DetectedStatus, patternName, cleanedText string)
Record appends a detection event to the ring buffer.
func (*DetectionEventSink) SetSessionID ¶ added in v1.35.0
func (s *DetectionEventSink) SetSessionID(id string)
SetSessionID sets the session identifier embedded in all future DetectionEvents.
type DetectorRegistry ¶ added in v1.35.0
type DetectorRegistry struct {
// contains filtered or unexported fields
}
DetectorRegistry maps binary name -> BinaryDetector.
func DefaultRegistry ¶ added in v1.35.0
func DefaultRegistry() *DetectorRegistry
DefaultRegistry returns a DetectorRegistry pre-populated with all built-in binary detectors.
func MergedRegistry ¶ added in v1.41.0
func MergedRegistry(builtins *DetectorRegistry, plugins []BinaryDetector) *DetectorRegistry
MergedRegistry returns a new DetectorRegistry containing every detector from builtins plus every detector in plugins. A plugin whose Name() matches a built-in (or another plugin already merged) replaces it in the result rather than growing the registry. builtins is never mutated — the caller's registry is left untouched.
func NewDetectorRegistry ¶ added in v1.35.0
func NewDetectorRegistry() *DetectorRegistry
NewDetectorRegistry creates a new empty DetectorRegistry.
func (*DetectorRegistry) Len ¶ added in v1.35.0
func (r *DetectorRegistry) Len() int
Len returns the number of registered detectors.
func (*DetectorRegistry) Lookup ¶ added in v1.35.0
func (r *DetectorRegistry) Lookup(name string) (BinaryDetector, bool)
Lookup returns the BinaryDetector for the given binary name, and whether it was found.
func (*DetectorRegistry) Names ¶ added in v1.35.0
func (r *DetectorRegistry) Names() []string
Names returns all registered binary names.
func (*DetectorRegistry) Register ¶ added in v1.35.0
func (r *DetectorRegistry) Register(d BinaryDetector)
Register adds a BinaryDetector to the registry. Panics if a detector with the same name has already been registered.
func (*DetectorRegistry) Upsert ¶ added in v1.41.0
func (r *DetectorRegistry) Upsert(d BinaryDetector)
Upsert adds or replaces a BinaryDetector in the registry, keyed by d.Name(). Unlike Register, Upsert never panics on a duplicate name — it silently overwrites the existing entry. This is the only sanctioned way to replace an entry: Register's panic is a deliberate guard against accidentally double-registering compiled-in detectors, while Upsert is meant for the override path where a plugin is expected to replace a built-in. Neither Register nor Upsert is safe for concurrent use; the registry is built once up front and then published via a snapshot elsewhere.
type IdleDetector ¶
type IdleDetector struct {
// contains filtered or unexported fields
}
IdleDetector monitors PTY output to determine if a Claude Code session is idle. It uses pattern matching on recent output and tracks state transitions with debouncing.
func NewIdleDetector ¶
func NewIdleDetector(sessionName string, ptyAccess PTYReader) *IdleDetector
NewIdleDetector creates a new idle detector for a session.
func NewIdleDetectorWithConfig ¶
func NewIdleDetectorWithConfig(sessionName string, ptyAccess PTYReader, config IdleDetectorConfig) *IdleDetector
NewIdleDetectorWithConfig creates a new idle detector with custom configuration.
func NewIdleDetectorWithDetector ¶ added in v1.35.0
func NewIdleDetectorWithDetector(sessionName string, ptyAccess PTYReader, config IdleDetectorConfig, detector TerminalDetector) *IdleDetector
NewIdleDetectorWithDetector creates a new idle detector that uses the provided TerminalDetector instead of creating its own. When detector is nil, falls back to creating a new StatusDetector (same as NewIdleDetectorWithConfig).
func (*IdleDetector) DetectState ¶
func (id *IdleDetector) DetectState() IdleState
DetectState analyzes recent PTY output and returns the current idle state. This method applies pattern matching and debouncing logic. DEPRECATED: Use DetectStateFromContent for more reliable detection. This method uses the PTY circular buffer which may contain incomplete data.
func (*IdleDetector) DetectStateFromContent ¶
func (id *IdleDetector) DetectStateFromContent(content string) IdleState
DetectStateFromContent analyzes provided terminal content and returns the current idle state. This method should be preferred over DetectState() as it allows the caller to provide reliable terminal content (e.g., from tmux capture-pane) instead of using the PTY circular buffer. Equivalent to DetectStateFromContentWithOSC(content, dtypes.OSCStatusNone).
func (*IdleDetector) DetectStateFromContentWithOSC ¶ added in v1.49.0
func (id *IdleDetector) DetectStateFromContentWithOSC(content string, osc dtypes.OSCStatus) IdleState
DetectStateFromContentWithOSC is DetectStateFromContent plus an OSC-derived overlay (gated by IsOSCExecutingPromotable/IsOSCIdlePromotable, shared with applyOSCStatusOverride's DetectedStatus side), resolved with exactly one lock-protected write — see osc-status-signals architecture-review.md BLOCKER 1 for why two sequential commits sharing lastStateChange is unsafe.
func (*IdleDetector) GetIdleDuration ¶
func (id *IdleDetector) GetIdleDuration() time.Duration
GetIdleDuration returns how long the session has been idle.
func (*IdleDetector) GetLastActivity ¶
func (id *IdleDetector) GetLastActivity() time.Time
GetLastActivity returns the timestamp of the last detected activity.
func (*IdleDetector) GetLastActivityNs ¶ added in v1.37.0
func (id *IdleDetector) GetLastActivityNs() int64
GetLastActivityNs returns the last activity time as Unix nanoseconds. Lock-free — reads the atomic shadow of lastActivity. Returns 0 when no activity has been recorded.
func (*IdleDetector) GetState ¶
func (id *IdleDetector) GetState() IdleState
GetState returns the current idle state without triggering detection. Use this when you want the cached state without analyzing PTY output.
func (*IdleDetector) GetStateInfo ¶
func (id *IdleDetector) GetStateInfo() IdleStateInfo
GetStateInfo returns comprehensive state information for debugging and display.
func (*IdleDetector) InitializeFromTimestamp ¶
func (id *IdleDetector) InitializeFromTimestamp(timestamp time.Time)
InitializeFromTimestamp restores the idle detector state from a persisted timestamp. This should be called immediately after creation when restoring a session from storage to maintain temporal continuity across server restarts.
This method prevents false "timeout" detection after server restarts by preserving the historical activity timeline. Without this restoration, all sessions would show "Timed out after Xs" immediately after restart because the idle detector initializes with time.Now() by default.
Parameters:
- timestamp: The last known activity timestamp (typically Instance.LastMeaningfulOutput)
Thread-safety: Safe to call concurrently (uses mutex)
Validation:
- Zero timestamps are ignored (no restoration)
- Future timestamps are rejected (clock skew protection)
- Very old timestamps (>24h) are rejected to prevent misleading timeout messages
func (*IdleDetector) RecordActivity ¶ added in v1.9.0
func (id *IdleDetector) RecordActivity()
RecordActivity updates lastActivity to now when PTY bytes arrive. It is debounced: if lastActivity was already updated within minActivityInterval, this is a no-op. This keeps the idle timer accurate while avoiding excessive cache invalidation in the review queue poller.
func (*IdleDetector) Reset ¶
func (id *IdleDetector) Reset()
Reset resets the idle detector's state tracking. Use this when reattaching to a session or after significant changes.
func (*IdleDetector) UpdateConfig ¶
func (id *IdleDetector) UpdateConfig(config IdleDetectorConfig)
UpdateConfig updates the idle detector configuration.
type IdleDetectorConfig ¶
type IdleDetectorConfig struct {
IdleThreshold time.Duration // Duration before considering session timed out
DebounceDelay time.Duration // Delay before changing state to prevent flickering
BufferSize int // Number of bytes to analyze from recent output
// OSCDebounceDelay gates OSC-derived transitions (DetectStateFromContentWithOSC)
// with a shorter window than DebounceDelay, sharing the same lastStateChange
// clock — see osc-status-signals ADR-002.
OSCDebounceDelay time.Duration
}
IdleDetectorConfig contains configuration for idle detection behavior.
func DefaultIdleDetectorConfig ¶
func DefaultIdleDetectorConfig() IdleDetectorConfig
DefaultIdleDetectorConfig returns sensible defaults for idle detection.
type IdleStateInfo ¶
type IdleStateInfo struct {
State IdleState
LastActivity time.Time
IdleDuration time.Duration
LastStateChange time.Time
SessionName string
}
IdleStateInfo contains comprehensive information about the current idle state.
func (IdleStateInfo) Description ¶
func (info IdleStateInfo) Description() string
Description returns a detailed description of the idle state info.
type PTYNormalizer ¶ added in v1.35.0
type PTYNormalizer struct{}
PTYNormalizer handles ANSI stripping and carriage-return collapsing for PTY output. It is a stateless struct; all methods are pure transformations.
func (PTYNormalizer) Normalize ¶ added in v1.35.0
func (n PTYNormalizer) Normalize(content string) string
Normalize strips ANSI escape sequences and collapses CR-overwritten segments. Equivalent to stripANSI(collapseCarriageReturns(content)).
func (PTYNormalizer) SplitLines ¶ added in v1.35.0
func (n PTYNormalizer) SplitLines(content string) []string
SplitLines splits normalized content into non-blank lines.
type PTYReader ¶
PTYReader provides access to recent terminal output. Implemented by *session.PTYAccess; defined here as an interface to avoid a circular import between session/detection and session.
type PatternSet ¶ added in v1.35.0
type PatternSet struct {
// contains filtered or unexported fields
}
PatternSet holds compiled regex slices for all StatusPatterns categories. Immutable after NewPatternSet returns — no lock needed.
func NewPatternSet ¶ added in v1.35.0
func NewPatternSet(p StatusPatterns) (*PatternSet, error)
NewPatternSet compiles all patterns in p. Returns an error if any regex is invalid.
func (*PatternSet) MatchLines ¶ added in v1.35.0
func (ps *PatternSet) MatchLines(text string, rawPTY []byte) (DetectedStatus, string, string, int)
MatchLines runs the pattern priority chain on the given text string and raw PTY bytes. Returns (status, patternName, description, subagentCount). subagentCount is only ever non-zero when the WaitingForAgent group wins — see matchWaitingForAgent.
func (*PatternSet) Patterns ¶ added in v1.35.0
func (ps *PatternSet) Patterns() StatusPatterns
Patterns returns the StatusPatterns used by this PatternSet.
type PluginDetector ¶ added in v1.41.0
type PluginDetector struct {
// contains filtered or unexported fields
}
PluginDetector implements dtypes.BinaryDetector from a validated plugin file: one instance per (file × binary name), sharing that file's compiled PatternSet (built once in LoadPluginDir — see CompiledPatternSet). See ADR-003 on why id and binary_names are separate concepts — id is provenance/collision identity, binary_names are the registry keys.
func (*PluginDetector) CompiledPatternSet ¶ added in v1.41.0
func (d *PluginDetector) CompiledPatternSet() *PatternSet
CompiledPatternSet returns the *PatternSet LoadPluginDir already compiled for this file, shared unchanged across every PluginDetector built from the same file's binary_names. buildSnapshot (detector_snapshot.go) type-asserts for this to avoid re-compiling the same regex strings once per binary name — a plugin file declaring N binary_names previously paid N redundant NewPatternSet compiles on every rebuild (including every periodic safety-net tick) for identical patterns.
func (*PluginDetector) FilterContent ¶ added in v1.41.0
func (d *PluginDetector) FilterContent(content string) string
FilterContent is the identity function: schema v1 plugin content carries no binary-specific content filtering (ADR-004 — plugin content is regex and identifiers only).
func (*PluginDetector) ID ¶ added in v1.41.0
func (d *PluginDetector) ID() string
ID returns the plugin's declared id, used for collision detection and log messages — not a registry key (see ADR-003).
func (*PluginDetector) Name ¶ added in v1.41.0
func (d *PluginDetector) Name() string
Name returns the binary name this detector instance was built for.
func (*PluginDetector) Patterns ¶ added in v1.41.0
func (d *PluginDetector) Patterns() dtypes.StatusPatterns
Patterns returns the status patterns compiled from the plugin file.
func (*PluginDetector) SourcePath ¶ added in v1.41.0
func (d *PluginDetector) SourcePath() string
SourcePath returns the absolute path of the .toml file this detector was loaded from, for provenance in logs and debugging.
type PluginLoadError ¶ added in v1.41.0
type PluginLoadError struct {
// Path is the plugin file this error concerns.
Path string
// Field is a path expression naming the offending key, e.g. "id",
// "binary_names[1]", or "patterns[0].regex". A directory-level read
// failure uses Field "directory"; the total-file-count cap uses the
// distinct Field "file_count" (not "directory") specifically so callers
// can treat "directory" as fatal ("scan failed, keep the previous
// snapshot") without also treating a successful partial scan as fatal.
Field string
// Err is the underlying reason. Unwrap returns it so callers can use
// errors.Is/As against, e.g., the concrete *regexp.Error a bad pattern
// produced.
Err error
}
PluginLoadError describes one rejected plugin file, or one rejected field within an otherwise-loadable file. LoadPluginDir accumulates these rather than treating any single bad file as fatal — one invalid file must not prevent other valid plugin files, or the built-in detectors, from loading.
func (PluginLoadError) Error ¶ added in v1.41.0
func (e PluginLoadError) Error() string
Error implements the error interface, naming the file, field, and reason.
func (PluginLoadError) Unwrap ¶ added in v1.41.0
func (e PluginLoadError) Unwrap() error
Unwrap exposes the underlying error for errors.Is/As.
type PluginWatcher ¶ added in v1.41.0
type PluginWatcher struct {
// contains filtered or unexported fields
}
PluginWatcher watches a detector plugin directory for changes and hot-reloads the active detector snapshot via rebuildSnapshot. It always watches the directory itself, never individual plugin files: editors typically save via write-temp-then-rename, which silently stops firing events on a per-file fsnotify watch after the first save — a well-known fsnotify caveat (see session/unfinished/watcher.go for the same directory- level pattern applied to .git dirs).
func StartPluginWatcher ¶ added in v1.41.0
func StartPluginWatcher(ctx context.Context, dir string) (*PluginWatcher, error)
StartPluginWatcher begins watching dir for detector plugin changes and returns immediately; the watch loop runs in its own goroutine and stops when ctx is cancelled. fsnotify being unavailable (NewWatcher or Add failing) is never fatal — StartPluginWatcher falls back to the periodic rescan alone and logs a warning, mirroring the fallback pattern in session/unfinished/watcher.go's NewWatchDirWatcher. The error return exists for API stability/future-proofing; every path today returns nil.
func (*PluginWatcher) Stopped ¶ added in v1.41.0
func (w *PluginWatcher) Stopped() <-chan struct{}
Stopped returns a channel that is closed once the watcher's goroutine has exited (context cancellation or a closed Events channel).
type StatusDetector ¶
type StatusDetector struct {
// contains filtered or unexported fields
}
StatusDetector analyzes PTY output to determine the current status of a Claude instance.
func NewStatusDetector ¶
func NewStatusDetector() *StatusDetector
NewStatusDetector creates a new status detector with default patterns.
func NewStatusDetectorFromFile ¶
func NewStatusDetectorFromFile(path string) (*StatusDetector, error)
NewStatusDetectorFromFile creates a status detector with patterns loaded from a YAML file.
func ResolveDetectorForProgram ¶ added in v1.41.0
func ResolveDetectorForProgram(program string) (*StatusDetector, bool)
ResolveDetectorForProgram returns a StatusDetector for program (built-in or user plugin) registered in the currently-active snapshot, or (nil, false) if none is registered — callers should fall back to NewStatusDetector() in that case. This is the first production call site for per-program detector resolution (ClaudeController.Start).
The returned detector is a fresh *StatusDetector that shares the snapshot's compiled PatternSet (immutable once built — safe to share across goroutines, see PatternSet's doc comment) but owns its own DetectionEventSink/normalizer. It deliberately does NOT return the snapshot's own *StatusDetector pointer: callers call SetSessionID on whatever they get back, which mutates DetectionEventSink.sessionID. Since every session running the same program (multiple concurrent "claude" sessions is the common case) would resolve to that one shared snapshot entry, handing out the shared pointer directly would let one session's SetSessionID silently reattribute another session's already-in-flight detection events.
func (*StatusDetector) Detect ¶
func (sd *StatusDetector) Detect(output []byte) DetectedStatus
Detect analyzes the provided PTY output and returns the detected status. Patterns are checked in priority order: Error > TestsFailing > NeedsApproval > InputRequired > WaitingForAgent > Success > Compacting > Active > Processing > Idle > Ready. Returns StatusUnknown if no patterns match.
func (*StatusDetector) DetectForProgram ¶ added in v1.12.0
func (sd *StatusDetector) DetectForProgram(output []byte, program string) DetectedStatus
DetectForProgram detects the status for output from a named program. When the program has a registered BinaryDetector, its per-binary pattern set is consulted first. If the per-binary detector returns StatusUnknown (no match), the generic Detect() is called as a fallback. For unregistered programs, only the generic Detect() is used.
func (*StatusDetector) DetectFromLines ¶
func (sd *StatusDetector) DetectFromLines(lines []string) DetectedStatus
DetectFromLines analyzes multiple lines of output and returns the most relevant status. Lines are processed in reverse order (most recent first) so the current terminal state takes precedence over stale scrollback content.
Blank/whitespace-only lines are skipped. StatusReady results are noted but the scan continues looking for a more specific status — this prevents the `.*` Ready catch-all from stopping the scan on an unrelated line (e.g. "PR #66") before reaching a real status pattern on an earlier line. StatusReady is returned as a fallback if no more specific status is found.
func (*StatusDetector) DetectFromString ¶
func (sd *StatusDetector) DetectFromString(output string) DetectedStatus
DetectFromString is a convenience method that accepts a string instead of []byte.
func (*StatusDetector) DetectRecent ¶
func (sd *StatusDetector) DetectRecent(output []byte, n int) DetectedStatus
DetectRecent analyzes the most recent n bytes of output for status detection. This is optimized for real-time status monitoring.
func (*StatusDetector) DetectWithContext ¶
func (sd *StatusDetector) DetectWithContext(output []byte) (DetectedStatus, string)
DetectWithContext returns the detected status along with a user-friendly context message. Uses the pattern's Description field for human-readable messages instead of raw matched text.
func (*StatusDetector) DetectWithContextAndCountFromLines ¶ added in v1.43.0
func (sd *StatusDetector) DetectWithContextAndCountFromLines(lines []string) (DetectedStatus, string, int)
DetectWithContextAndCountFromLines is the count-aware sibling of DetectWithContextFromLines. It returns the same status and context, plus the subagent/shell/monitor count captured from the winning WaitingForAgent match (0 for any other status). Added as a new method rather than changing DetectWithContextFromLines in place because that method is pinned by the TerminalDetector interface and consumed by review_queue_determiner.go plus several test files that don't need the count.
func (*StatusDetector) DetectWithContextFromLines ¶ added in v1.35.0
func (sd *StatusDetector) DetectWithContextFromLines(lines []string) (DetectedStatus, string)
DetectWithContextFromLines analyzes lines in reverse order (most recent first) and returns the detected status with context. This ensures current terminal state (e.g. "? for shortcuts" on the last line) takes precedence over stale scrollback content (e.g. an old "esc to interrupt" from a previous turn that is still within the scanned window).
Blank/whitespace-only lines are skipped. StatusReady is treated as a low-confidence fallback — the scan continues past Ready results looking for a more specific status, preventing the `.*` catch-all from masking real patterns on earlier lines.
Signature intentionally left unchanged (pinned by the TerminalDetector interface and its callers) — see DetectWithContextAndCountFromLines for the count-aware sibling.
func (*StatusDetector) ExportPatterns ¶
func (sd *StatusDetector) ExportPatterns(path string) error
ExportPatterns exports the current patterns to a YAML file. Note: the exported YAML does not (and cannot) capture the auto-mode footer override — see LoadPatterns' doc comment and autoModeFooterRegex, above.
func (*StatusDetector) GetPatternNames ¶
func (sd *StatusDetector) GetPatternNames(status DetectedStatus) []string
GetPatternNames returns the names of all loaded patterns for a given status.
func (*StatusDetector) HasPattern ¶
func (sd *StatusDetector) HasPattern(status DetectedStatus, name string) bool
HasPattern checks if a specific pattern name exists for the given status.
func (*StatusDetector) LoadPatterns ¶
func (sd *StatusDetector) LoadPatterns(path string) error
LoadPatterns loads patterns from a YAML file. Note: this does NOT cover the auto-mode footer override (autoModeFooterRegex / applyFooterIdleOverride, above) — that check always runs regardless of any custom pattern file loaded here, for the reasons given in autoModeFooterRegex's doc comment.
func (*StatusDetector) RecentEvents ¶ added in v1.35.0
func (sd *StatusDetector) RecentEvents(n int) []DetectionEvent
RecentEvents returns up to n most-recent DetectionEvents, newest-first.
func (*StatusDetector) SetSessionID ¶ added in v1.35.0
func (sd *StatusDetector) SetSessionID(id string)
SetSessionID sets the session identifier embedded in all future DetectionEvents. Call this once after creating the detector, before any detections run.
type StatusPattern ¶
type StatusPattern = dtypes.StatusPattern
StatusPattern represents a regex pattern for detecting a specific status. This is a type alias for dtypes.StatusPattern to avoid import cycles while keeping the type accessible from this package without qualification.
type StatusPatterns ¶
type StatusPatterns = dtypes.StatusPatterns
StatusPatterns contains all patterns for status detection. This is a type alias for dtypes.StatusPatterns.
type TerminalDetector ¶ added in v1.35.0
type TerminalDetector interface {
Detect(output []byte) DetectedStatus
DetectWithContext(output []byte) (DetectedStatus, string)
DetectWithContextFromLines(lines []string) (DetectedStatus, string)
DetectFromLines(lines []string) DetectedStatus
RecentEvents(n int) []DetectionEvent
SetSessionID(id string)
}
TerminalDetector is the interface implemented by StatusDetector that consumer packages (session, server/services) depend on. Defined here (consumer-of-input pattern) so all call sites can use the interface without importing the concrete type.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package binaries provides per-binary BinaryDetector implementations.
|
Package binaries provides per-binary BinaryDetector implementations. |
|
Package dtypes contains shared types for the detection package and its sub-packages.
|
Package dtypes contains shared types for the detection package and its sub-packages. |