domain

package
v0.222.2 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Index

Constants

View Source
const EnvSubagentAgentMode = "INFER_SUBAGENT_AGENT_MODE"

EnvSubagentAgentMode names the environment variable the Agent tool sets to carry the parent chat's coding mode (the AgentMode.ModeKey form - "standard"/"plan"/"auto"/"auto-with-judge") to a spawned subagent, so it starts in the same mode as its parent. It is absent for top-level `infer chat`/`infer headless` runs, which therefore stay Standard-by-default.

View Source
const EnvSubagentApprovalFile = "INFER_SUBAGENT_APPROVAL_FILE"

EnvSubagentApprovalFile names the environment variable the Agent tool sets on an interactive subagent's `infer chat` so it writes a SubagentApprovalFile JSON to that path whenever it blocks on a tool-approval prompt (and removes it when the prompt resolves). The parent watches this file to surface "subagent is awaiting approval" to the user and relay the decision (ApproveSubagent). Unset for normal `infer chat`, which writes nothing.

View Source
const EnvSubagentHistoryName = "INFER_SUBAGENT_HISTORY_NAME"

EnvSubagentHistoryName names the environment variable the Agent tool sets on an interactive subagent's `infer chat` so it uses its own history file (<configDir>/history/history-<name>) instead of the main agent's history. When unset or empty, the subagent falls back to the main history file; the reserved value SubagentHistoryMemoryOnly selects in-memory-only history.

View Source
const EnvSubagentResultFile = "INFER_SUBAGENT_RESULT_FILE"

EnvSubagentResultFile names the environment variable the Agent tool sets on an interactive subagent's `infer chat` so it writes its last assistant message (as a SubagentResultFile JSON) to that path on each completed turn. The parent reads it to deliver the subagent's real answer - not the tmux pane's chrome - when the subagent finishes. Unset for normal `infer chat`, which writes nothing.

View Source
const SubagentHistoryMemoryOnly = ":memory:"

SubagentHistoryMemoryOnly is the reserved EnvSubagentHistoryName value telling an interactive subagent to keep its input history in memory only (no file). The Agent tool sets it for subagents without a usable label so they don't create a new single-use history file per spawn. sanitizeSlug never yields this value (it contains ':'), so it can't collide with a real slug.

View Source
const SubagentTurnLineType = "subagent_turn"

SubagentTurnLineType is the type of the line a keep-alive headless subagent prints on stdout after each of its turns.

Variables

CronParser accepts the schedules a ScheduledJob may carry: standard 5-field expressions and @descriptors. The scheduler runs jobs with the same parser.

Functions

func InheritedAgentMode added in v0.178.1

func InheritedAgentMode() agentdomain.AgentMode

InheritedAgentMode returns the coding mode a subagent should start in, read from EnvSubagentAgentMode. Falls back to standard mode when unset or unparseable.

func ParseCron added in v0.207.0

func ParseCron(expr string) error

ParseCron reports whether expr is a valid job schedule.

func ValidJobID added in v0.182.1

func ValidJobID(id string) bool

ValidJobID reports whether id is safe to use as a single file-name component. Job IDs are UUIDs at creation, but they also arrive as raw LLM tool arguments and become file paths (schedules dir, workflow files), so anything containing path separators or dot-segments is rejected to prevent path traversal.

func WriteSubagentResultFile added in v0.207.0

func WriteSubagentResultFile(path string, rf SubagentResultFile) error

WriteSubagentResultFile writes rf to path through a temp file and rename, so a polling parent never reads a half-written file.

Types

type BackgroundJob

type BackgroundJob interface {
	// Meta returns the job's identity/display snapshot.
	Meta() JobMeta
	// Run blocks until the job reaches a terminal state or ctx is cancelled,
	// emitting intermediate JobSignals via emit, and returns the terminal result.
	// It MUST return promptly once ctx is cancelled.
	Run(ctx context.Context, emit func(JobSignal)) agentdomain.ToolExecutionResult
	// Wind delivers a graceful wind-down (WindWrapUp) or hard stop (WindStop) to a
	// RUNNING job. It must be safe to call from another goroutine while Run is in
	// flight, be idempotent, and a no-op once the job has finished.
	Wind(ctx context.Context, sig WindSignal) error
	// Close tears down the resources the job owns (its per-kind tracker record, an
	// interactive subagent's tmux pane and temp files, ...). The supervisor calls
	// it exactly once when it reaps a finished job after the retention window. It
	// must be idempotent and must not touch the running process (use Wind for that).
	Close()
}

BackgroundJob is one unit of monitorable background work. The supervisor owns the single goroutine that calls Run plus all the lifecycle around it (events, queue notification, tracking, cleanup); a job only has to (a) block until it reaches a terminal state and return the outcome, and (b) honour wind-down/stop signals. This is the seam that lets A2A tasks, shells, and subagents share one fan-in implementation - each kind differs only in how it learns it is done.

type BackgroundShell

type BackgroundShell struct {
	ShellID      string
	Command      string
	Cmd          *exec.Cmd
	StartedAt    time.Time
	CompletedAt  *time.Time
	State        ShellState
	ExitCode     *int
	OutputBuffer OutputRingBuffer
	CancelFunc   context.CancelFunc
	ReadOffset   int64

	ReadersDone <-chan struct{}
}

BackgroundShell represents a command running in the background.

type BackgroundShellService

type BackgroundShellService interface {
	// DetachToBackground moves a running command to background. readersDone, when
	// non-nil, is closed once the caller's pipe readers reach EOF; the supervisor
	// waits on it before reaping so trailing output is not truncated.
	DetachToBackground(ctx context.Context, cmd *exec.Cmd, command string, outputBuffer OutputRingBuffer, readersDone <-chan struct{}) (string, error)

	// GetShellOutput retrieves output from a shell
	GetShellOutput(shellID string, fromOffset int64) (string, int64, ShellState, error)

	// GetShellOutputWithFilter retrieves filtered output from a shell
	GetShellOutputWithFilter(shellID string, fromOffset int64, filterPattern string) (string, int64, ShellState, error)

	// GetShell returns a specific shell by ID
	GetShell(shellID string) *BackgroundShell

	// GetAllShells returns all tracked shells
	GetAllShells() []*BackgroundShell

	// CancelShell cancels a running background shell
	CancelShell(shellID string) error

	// RemoveShell removes a shell from tracking
	RemoveShell(shellID string) error
}

BackgroundShellService defines the interface for managing background shells

type BackgroundTaskRegistry

type BackgroundTaskRegistry interface {
	ShellTracker
	SubagentTracker

	// HasPending reports whether *any* background work is still in flight,
	// regardless of type. True when there is at least one session-holding job
	// running (an A2A task, a background shell, or a HEADLESS subagent).
	// It deliberately excludes interactive subagents so a one-shot `infer headless`
	// does not hang at exit waiting on a user-driven tmux pane.
	HasPending() bool

	// Submit hands a background job to the supervisor, which spawns its monitor
	// goroutine and folds its result back onto the conversation when it finishes.
	// This is the single entry point every kind (A2A task, shell, subagent) uses
	// instead of running its own poller.
	Submit(job BackgroundJob)

	// Snapshot returns the supervisor's view of all live and recently-finished
	// jobs for the task view and status line.
	Snapshot() []TrackedJob

	// CountRunningJobs returns how many supervised jobs are running, optionally
	// filtered to one kind (pass "" for all kinds).
	CountRunningJobs(kind JobKind) int

	// IsJobRunning reports whether the supervised job with the given id is still
	// running. It is the per-id liveness query a tool uses (via the narrow
	// JobLivenessReporter projection) to defer to the supervisor - the single
	// source of truth - instead of racing it with a manual read.
	IsJobRunning(id string) bool

	// WindJob sends a graceful wind-down or hard stop to one supervised job.
	WindJob(id string, sig WindSignal) error
}

BackgroundTaskRegistry is the single tracker that owns *all* in-flight background work an agent session can produce: background bash shells and subagents, plus every job submitted to the supervisor (such as a task delegated to an A2A agent). Callers that need less depend on the narrower ShellTracker or SubagentTracker, or on a Job* projection.

type JobFinisher added in v0.216.0

type JobFinisher interface {
	Finished(result agentdomain.ToolExecutionResult)
}

JobFinisher is an optional BackgroundJob extension the supervisor calls once with the terminal result of a job it did not discard, before the task view refreshes. An A2A task uses it to keep its outcome listed in the task view after its monitor goroutine exits.

type JobIdleReporter added in v0.222.0

type JobIdleReporter interface {
	BackgroundJob
	Idle() bool
	IdleSince() time.Time
}

JobIdleReporter is a BackgroundJob that stays running between turns, like a headless subagent awaiting a follow-up. While Idle reports true the job does not hold the session. IdleSince is when its last turn ended, zero while busy, so a reader can freeze the job's elapsed time while it idles.

type JobKind

type JobKind string

JobKind identifies which background-work subsystem produced a job so one supervisor, one tracker, and one task view can treat A2A tasks, background shells, and subagents uniformly while still reporting per-kind counts.

const (
	JobKindA2A       JobKind = "a2a"
	JobKindShell     JobKind = "shell"
	JobKindSubagent  JobKind = "subagent"
	JobKindRecording JobKind = "recording"
)

type JobLivenessReporter

type JobLivenessReporter interface {
	IsJobRunning(id string) bool
}

JobLivenessReporter reports whether a supervised background job is still running, by id. It is the narrow projection of BackgroundTaskRegistry a tool uses to tell whether the supervisor is still driving a job it launched - an A2A task being polled, a background shell, or a subagent - so a manual read defers to the supervisor (the single source of truth) instead of racing it.

type JobMeta

type JobMeta struct {
	ID           string
	SessionID    string
	Kind         JobKind
	Origin       string
	Label        string
	Description  string
	Detail       string
	StartedAt    time.Time
	Silent       bool
	HoldsSession bool
}

JobMeta is the identity/display snapshot a background job exposes. The supervisor reads it once at submit (and surfaces it in the task view); it is not on any hot path. Origin says where the work runs for a kind that has more than one place, and is empty otherwise. SessionID names the stored conversation of a job that keeps one, so it outlives the job's tracker entry.

type JobNotifier

type JobNotifier interface {
	Notification(result agentdomain.ToolExecutionResult) string
}

JobNotifier is an optional BackgroundJob extension. A job that implements it formats its own completion-notification body (the text enqueued for the agent to read when it finishes) - e.g. a shell reports its exit code and duration, which a generic tool-result formatter would not. Jobs that do not implement it get the default domain formatting of their ToolExecutionResult.

type JobOutputProvider

type JobOutputProvider interface {
	Output() string
}

JobOutputProvider is an optional BackgroundJob extension. A job that implements it provides its output text for the /tasks detail panel - e.g. a shell's captured stdout/stderr or a subagent's final result. Jobs that do not implement it show no output section in the detail panel.

type JobPatch added in v0.207.0

type JobPatch struct {
	Name           *string
	Description    *string
	CronExpression *string
	Prompt         *string
	Model          *string
	RunOnce        *bool
}

JobPatch carries edits to a ScheduledJob; a nil field is left unchanged.

type JobSignal

type JobSignal struct {
	// Note is a human-facing line surfaced to the UI and, when Enqueue is set,
	// landed on the message queue so the agent reads it on its next turn.
	Note string
	// Enqueue lands Note on the message queue and wakes the agent loop; otherwise
	// Note is a UI-only status update.
	Enqueue bool
	// State is an optional kind-specific status token (e.g. an A2A task state).
	State string
}

JobSignal is an intermediate, non-terminal event a running job emits to the supervisor (an A2A status change, a subagent that became blocked on a tool approval). The terminal outcome is Run's return value, never a signal.

type JobStatsProvider added in v0.220.0

type JobStatsProvider interface {
	Stats() *SubagentRunStats
}

JobStatsProvider is an optional BackgroundJob extension. A subagent or A2A job implements it to offer its run stats, live while it runs when it can tally them and nil until it has any.

type JobStatus

type JobStatus string

JobStatus is the unified lifecycle state across every background-work kind.

const (
	JobRunning   JobStatus = "running"
	JobCompleted JobStatus = "completed"
	JobFailed    JobStatus = "failed"
)

func (JobStatus) IsTerminal

func (s JobStatus) IsTerminal() bool

IsTerminal reports whether the job has finished (completed or failed).

type JobStopper

type JobStopper interface {
	WindJob(id string, sig WindSignal) error
}

JobStopper ends a supervised background job by id. It is the narrow projection of BackgroundTaskRegistry that CloseSubagent uses to wind down the supervised monitor of the subagent it closes - cancelling the job's Run context so the status-line running-count drops immediately instead of lingering until the pane-watcher next polls (or never, if a killed pane is not observed as gone).

type JobSubmitter

type JobSubmitter interface {
	Submit(job BackgroundJob)
}

JobSubmitter hands a background job to the supervisor. It is the narrow projection of BackgroundTaskRegistry that tools use to submit work without depending on the whole registry surface.

type OutputRingBuffer

type OutputRingBuffer interface {
	Write(p []byte) (n int, err error)
	ReadFrom(offset int64) (string, int64)
	Recent(maxBytes int) string
	TotalWritten() int64
	Size() int
	String() string
	Clear()
}

OutputRingBuffer defines the interface for the circular output buffer.

type PaneObservation

type PaneObservation struct {
	// Harvested is the subagent chat's real last assistant message (from its
	// result file); "" until its turn completes. The ONLY content ever delivered -
	// the pane is never scraped for content (its TUI chrome is noise).
	Harvested string
	// Screen is a snapshot of the pane's current tail, used by the poller to detect
	// idleness by stability: while the subagent works the chat's elapsed-time
	// spinner changes this every poll; at idle it is frozen. The input-box
	// placeholder ("Type your message") is NOT a usable idle signal - it is drawn
	// even mid-turn - so the stability of the whole tail is used instead.
	Screen string
	// Gone means the pane no longer exists (closed).
	Gone bool
	// Dead means the pane's process exited (the pane is kept open by remain-on-exit).
	Dead bool
	// AwaitingApproval means the subagent is blocked on a tool-approval prompt.
	AwaitingApproval bool
	// ApprovalSummary describes the pending tool call (name + args) when awaiting.
	ApprovalSummary string
	// Done is the result file's done flag: the subagent explicitly marked this
	// write as its task's terminal turn.
	Done bool
	// HarvestFailed means the harvested turn was a failed terminal turn
	// (the result file recorded Success=false).
	HarvestFailed bool
	// HarvestError is the error the subagent recorded for the harvested turn.
	HarvestError string
	// HarvestStats is the run's tool and token tally, nil when none was recorded.
	HarvestStats *SubagentRunStats
}

PaneObservation is one probe of an interactive subagent's tmux pane, produced by a pane inspector and consumed by the interactive subagent monitor (interactiveSubagentJob) to decide when a turn completed or an approval is pending.

type RunEvent

type RunEvent struct {
	Line []byte
	Err  error
	Done bool
}

RunEvent is emitted by the scheduler as a job run progresses. Line events carry one raw agent stdout line (valid only for the duration of the callback); the terminal event has Done set, with Err populated on failure.

type RunRecord

type RunRecord struct {
	SessionID  string     `yaml:"session_id" json:"session_id"`
	JobID      string     `yaml:"job_id" json:"job_id"`
	Status     RunStatus  `yaml:"status" json:"status"`
	Error      string     `yaml:"error,omitempty" json:"error,omitempty"`
	StartedAt  time.Time  `yaml:"started_at" json:"started_at"`
	FinishedAt *time.Time `yaml:"finished_at,omitempty" json:"finished_at,omitempty"`
}

RunRecord is the persisted record of one scheduled-job fire. SessionID is both the record key and the conversation ID of the `infer headless` run, so consumers (e.g. the desktop app) can load the full transcript from conversation storage.

type RunStatus

type RunStatus string

RunStatus is the lifecycle state of a single scheduled-job run.

const (
	RunStatusRunning   RunStatus = "running"
	RunStatusCompleted RunStatus = "completed"
	RunStatusFailed    RunStatus = "failed"
)

type ScheduledJob

type ScheduledJob struct {
	ID             string     `yaml:"id" json:"id"`
	Name           string     `yaml:"name,omitempty" json:"name,omitempty"`
	Description    string     `yaml:"description,omitempty" json:"description,omitempty"`
	CronExpression string     `yaml:"cron_expression" json:"cron_expression"`
	Prompt         string     `yaml:"prompt" json:"prompt"`
	Channel        string     `yaml:"channel,omitempty" json:"channel,omitempty"`
	RecipientID    string     `yaml:"recipient_id,omitempty" json:"recipient_id,omitempty"`
	Model          string     `yaml:"model,omitempty" json:"model,omitempty"`
	RunOnce        bool       `yaml:"run_once,omitempty" json:"run_once,omitempty"`
	CreatedAt      time.Time  `yaml:"created_at" json:"created_at"`
	UpdatedAt      time.Time  `yaml:"updated_at" json:"updated_at"`
	LastRun        *time.Time `yaml:"last_run,omitempty" json:"last_run,omitempty"`
	LastError      string     `yaml:"last_error,omitempty" json:"last_error,omitempty"`
}

ScheduledJob describes a task that the LLM has asked the system to run on a cron schedule. Jobs are persisted through the configured storage backend and executed by the scheduler running inside the `infer daemon` process.

Each fire spawns a fresh `infer headless` subprocess with its own session ID - no context is carried between fires. Channel/RecipientID are an optional delivery target: when set, run output is forwarded to that channel; when empty, the run is record-only and its output lives in storage (run record + conversation).

func NewScheduledJob added in v0.207.0

func NewScheduledJob(spec ScheduledJob, now time.Time) (*ScheduledJob, error)

NewScheduledJob returns spec as a new job: a fresh ID and creation stamp, with the schedule validated.

func (*ScheduledJob) Apply added in v0.207.0

func (j *ScheduledJob) Apply(p JobPatch, now time.Time) (bool, error)

Apply validates and applies p, stamping UpdatedAt. It reports false (and changes nothing) when p carries no edits.

type ShellInfo

type ShellInfo struct {
	ShellID     string
	Command     string
	State       ShellState
	StartedAt   time.Time
	CompletedAt *time.Time
	ExitCode    *int
	OutputSize  int64
	Elapsed     time.Duration
}

ShellInfo provides summary information about a shell for UI display.

func NewShellInfo

func NewShellInfo(shell *BackgroundShell) *ShellInfo

NewShellInfo creates a ShellInfo from a BackgroundShell.

type ShellState

type ShellState string

ShellState represents the state of a background shell.

const (
	ShellStateRunning   ShellState = "running"
	ShellStateCompleted ShellState = "completed"
	ShellStateFailed    ShellState = "failed"
	ShellStateCancelled ShellState = "cancelled"
)

func (ShellState) IsTerminal

func (s ShellState) IsTerminal() bool

IsTerminal returns true if the state is a terminal state (completed, failed, or cancelled).

func (ShellState) String

func (s ShellState) String() string

String returns the string representation of the shell state.

type ShellTracker

type ShellTracker interface {
	// Add adds a new shell to the tracker.
	// Returns an error if max concurrent limit is reached.
	Add(shell *BackgroundShell) error

	// Get retrieves a shell by ID.
	// Returns nil if not found.
	Get(shellID string) *BackgroundShell

	// GetAll returns all tracked shells.
	GetAll() []*BackgroundShell

	// Remove removes a shell from the tracker.
	Remove(shellID string) error

	// CountRunning returns the number of shells in running state.
	CountRunning() int
}

ShellTracker defines the interface for managing background shells.

type SubagentApprovalFile

type SubagentApprovalFile struct {
	Awaiting bool   `json:"awaiting"`
	Summary  string `json:"summary,omitempty"`
}

SubagentApprovalFile is the JSON an interactive subagent's chat writes while it is blocked on a tool-approval prompt. It is an authoritative signal (written the moment the chat blocks, removed when it resolves) so the parent does not have to scrape the pane's TUI to detect a pending approval.

type SubagentMode added in v0.220.0

type SubagentMode string

SubagentMode selects how a subagent is surfaced while it runs.

const (
	SubagentModeHeadless    SubagentMode = "headless"
	SubagentModeInteractive SubagentMode = "interactive"
)

type SubagentResultFile

type SubagentResultFile struct {
	FinalAssistant string            `json:"final_assistant"`
	Success        bool              `json:"success"`
	Error          string            `json:"error,omitempty"`
	SessionID      string            `json:"session_id,omitempty"`
	Done           bool              `json:"done,omitempty"`
	Stats          *SubagentRunStats `json:"stats,omitempty"`
}

SubagentResultFile is the JSON written by `infer headless --result-file` on exit and read back by the Agent tool to harvest a subagent's outcome from a detached (tmux) run whose stdout the parent does not own.

func ReadSubagentResultFile added in v0.207.0

func ReadSubagentResultFile(path string) (SubagentResultFile, bool)

ReadSubagentResultFile reads and parses a subagent result file without waiting. Returns ok=false when the file is absent or malformed.

type SubagentRunStats added in v0.220.0

type SubagentRunStats struct {
	ToolsSucceeded int `json:"tools_succeeded"`
	ToolsFailed    int `json:"tools_failed"`
	InputTokens    int `json:"input_tokens"`
	OutputTokens   int `json:"output_tokens"`
	CachedTokens   int `json:"cached_tokens,omitempty"`
}

SubagentRunStats is what a subagent run cost and how its tool calls went, so the parent can weigh an answer by the work behind it. CachedTokens is the slice of InputTokens the provider served from its prompt cache, and stays zero when it reports no cache hits.

func (SubagentRunStats) String added in v0.220.0

func (s SubagentRunStats) String() string

String renders the stats as the one line the parent agent reads.

type SubagentState

type SubagentState struct {
	ID          string
	Label       string
	Description string
	Model       string
	Mode        SubagentMode
	SessionID   string
	PaneID      string
	Status      SubagentStatus
	StartedAt   time.Time
	CancelFunc  context.CancelFunc
	Silent      bool
	Input       func(text string) error
}

SubagentState is the data record for one local subagent (an `infer headless` subprocess or tmux pane spawned by the Agent tool) that the subagent control tools (ListSubagents, CloseSubagent, ...) read. Monitoring is owned by the job supervisor (headlessSubagentJob / interactiveSubagentJob), not this struct.

type SubagentStatus

type SubagentStatus string

SubagentStatus represents the lifecycle state of a local subagent.

const (
	SubagentRunning   SubagentStatus = "running"
	SubagentCompleted SubagentStatus = "completed"
	SubagentFailed    SubagentStatus = "failed"
)

type SubagentTracker

type SubagentTracker interface {
	// AddSubagent registers a running subagent. Returns an error if the ID
	// is already tracked.
	AddSubagent(state *SubagentState) error

	// GetSubagent returns a subagent by ID, or nil if not tracked.
	GetSubagent(id string) *SubagentState

	// GetAllSubagents returns all tracked subagents.
	GetAllSubagents() []*SubagentState

	// RemoveSubagent removes a subagent from tracking.
	RemoveSubagent(id string) error

	// CountRunningSubagents returns the number of subagents in the running state.
	CountRunningSubagents() int

	// SetSubagentStatus atomically updates a subagent's status under the
	// tracker's lock. Returns an error if the ID is not tracked.
	SetSubagentStatus(id string, status SubagentStatus) error
}

SubagentTracker tracks local subagents spawned by the Agent tool. It is a projection of BackgroundTaskRegistry next to ShellTracker. Its methods are suffixed with "Subagent" to avoid colliding with the shell tracker's same-named surface when embedded together.

type SubagentTurnLine added in v0.222.0

type SubagentTurnLine struct {
	Type string `json:"type"`
	SubagentResultFile
}

SubagentTurnLine reports one finished turn of a keep-alive headless subagent to the parent that owns its stdout. Done is false when the subagent already has the next turn queued.

type TitleGenerator

type TitleGenerator interface {
	ProcessPendingTitles(ctx context.Context) error
}

TitleGenerator interface for conversation title generation

type TrackedJob

type TrackedJob struct {
	Meta        JobMeta
	Status      JobStatus
	CompletedAt *time.Time
	Output      string
	Stats       *SubagentRunStats
}

TrackedJob is a point-in-time snapshot of one supervised job for the task view and status line.

type WindSignal

type WindSignal int

WindSignal is the one-directional graceful control signal the supervisor pushes into a running job. WindWrapUp asks it to start finishing (inject a wind-down prompt, SIGTERM, or cancel the remote task); WindStop terminates it now (kill pane, SIGKILL, cancel). Graceful shutdown sends WindWrapUp to all jobs, waits a grace window, then WindStop. The supervisor also uses WindStop as the teardown when it reaps a finished job.

const (
	WindWrapUp WindSignal = iota
	WindStop
)

func (WindSignal) String

func (w WindSignal) String() string

String renders the signal for logs.

Jump to

Keyboard shortcuts

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