Documentation
¶
Index ¶
- Constants
- func ClaudeQuotaCachePath() (string, error)
- func ClaudeQuotaSnapshotAge(snapshot *ClaudeQuotaSnapshot, now time.Time) time.Duration
- func DefaultClaudeModelDiscovery() harnesses.ModelDiscoverySnapshot
- func IsClaudeQuotaExhaustedMessage(text string) bool
- func IsClaudeQuotaFresh(snapshot *ClaudeQuotaSnapshot, now time.Time, staleAfter time.Duration) bool
- func MarkClaudeQuotaExhaustedFromMessage(text string, now time.Time) bool
- func ReadClaudeModelDiscoveryFromCassette(dir string) (harnesses.ModelDiscoverySnapshot, error)
- func ReadClaudeModelDiscoveryViaPTY(timeout time.Duration, opts ...QuotaPTYOption) (harnesses.ModelDiscoverySnapshot, error)
- func ReadClaudeQuotaFromCassette(dir string) ([]harnesses.QuotaWindow, *harnesses.AccountInfo, error)
- func ReadClaudeQuotaViaPTY(timeout time.Duration, opts ...QuotaPTYOption) ([]harnesses.QuotaWindow, *harnesses.AccountInfo, error)
- func ReadClaudeQuotaViaTmux(timeout time.Duration) ([]harnesses.QuotaWindow, *harnesses.AccountInfo, error)deprecated
- func ReadClaudeReasoningFromHelp(ctx context.Context, binary string, args ...string) ([]string, error)
- func RefreshClaudeQuotaAsync(capture func() (ClaudeQuotaSnapshot, error))
- func ResolveClaudeFamilyAlias(model string, snapshot harnesses.ModelDiscoverySnapshot) string
- func WriteClaudeQuota(path string, snapshot ClaudeQuotaSnapshot) error
- type ClaudeQuotaRoutingDecision
- type ClaudeQuotaSnapshot
- type QuotaPTYOption
- type Runner
Constants ¶
const ClaudeModelDiscoveryFreshnessWindow = 24 * time.Hour
const DefaultClaudeQuotaStaleAfter = 15 * time.Minute
DefaultClaudeQuotaStaleAfter is the default maximum age before a cached snapshot is considered stale and foreground routing should fall back to the safe default.
Variables ¶
This section is empty.
Functions ¶
func ClaudeQuotaCachePath ¶
ClaudeQuotaCachePath returns the durable location for the Claude quota cache. It resolves to $XDG_STATE_HOME/<config-dir>/claude-quota.json, or ~/.local/state/<config-dir>/claude-quota.json when XDG_STATE_HOME is unset. The FIZEAU_CLAUDE_QUOTA_CACHE env var takes precedence (primarily for tests).
func ClaudeQuotaSnapshotAge ¶
func ClaudeQuotaSnapshotAge(snapshot *ClaudeQuotaSnapshot, now time.Time) time.Duration
ClaudeQuotaSnapshotAge reports the age of a snapshot relative to now. A zero or future CapturedAt yields a zero age.
func DefaultClaudeModelDiscovery ¶
func DefaultClaudeModelDiscovery() harnesses.ModelDiscoverySnapshot
func IsClaudeQuotaExhaustedMessage ¶ added in v0.10.5
IsClaudeQuotaExhaustedMessage recognizes Claude CLI quota failures that are emitted as plain text rather than structured quota data. Claude currently reports weekly exhaustion with wording like "out of extra usage", so callers must treat these strings as a hard quota signal.
func IsClaudeQuotaFresh ¶
func IsClaudeQuotaFresh(snapshot *ClaudeQuotaSnapshot, now time.Time, staleAfter time.Duration) bool
IsClaudeQuotaFresh reports whether a snapshot exists and is newer than staleAfter relative to now. A nil snapshot is never fresh. A zero staleAfter falls back to DefaultClaudeQuotaStaleAfter.
func MarkClaudeQuotaExhaustedFromMessage ¶ added in v0.10.5
MarkClaudeQuotaExhaustedFromMessage records a runtime Claude quota failure in the durable cache so later automatic routing avoids Claude until a fresh quota probe proves headroom again.
func ReadClaudeModelDiscoveryFromCassette ¶
func ReadClaudeModelDiscoveryFromCassette(dir string) (harnesses.ModelDiscoverySnapshot, error)
func ReadClaudeModelDiscoveryViaPTY ¶
func ReadClaudeModelDiscoveryViaPTY(timeout time.Duration, opts ...QuotaPTYOption) (harnesses.ModelDiscoverySnapshot, error)
func ReadClaudeQuotaFromCassette ¶
func ReadClaudeQuotaFromCassette(dir string) ([]harnesses.QuotaWindow, *harnesses.AccountInfo, error)
func ReadClaudeQuotaViaPTY ¶
func ReadClaudeQuotaViaPTY(timeout time.Duration, opts ...QuotaPTYOption) ([]harnesses.QuotaWindow, *harnesses.AccountInfo, error)
func ReadClaudeQuotaViaTmux
deprecated
func ReadClaudeQuotaViaTmux(timeout time.Duration) ([]harnesses.QuotaWindow, *harnesses.AccountInfo, error)
ReadClaudeQuotaViaTmux starts claude in a detached tmux session, sends /usage, captures the pane output, and returns parsed quota windows and account info.
Deprecated: this is a diagnostic-only legacy path. Supported quota probes use ReadClaudeQuotaViaPTY so accepted evidence passes through direct PTY cassettes. Returns an error if tmux or claude are not found, or probing times out.
func RefreshClaudeQuotaAsync ¶
func RefreshClaudeQuotaAsync(capture func() (ClaudeQuotaSnapshot, error))
RefreshClaudeQuotaAsync launches a background goroutine that invokes capture and writes the result to the default cache path. It returns immediately; callers never block on capture.
The capture function is injected so that the durable cache layer does not depend on any specific PTY implementation. A future `ddx claude refresh-quota` subcommand can pass a real PTY-backed capture; tests can pass deterministic fakes.
If capture returns an error, the cache is not touched.
func ResolveClaudeFamilyAlias ¶
func ResolveClaudeFamilyAlias(model string, snapshot harnesses.ModelDiscoverySnapshot) string
func WriteClaudeQuota ¶
func WriteClaudeQuota(path string, snapshot ClaudeQuotaSnapshot) error
WriteClaudeQuota atomically persists a ClaudeQuotaSnapshot to the given path. The parent directory is created if necessary. The file is written to a sibling .tmp file and renamed into place so readers never observe a partially-written snapshot. The final file mode is 0600.
Types ¶
type ClaudeQuotaRoutingDecision ¶
type ClaudeQuotaRoutingDecision struct {
// PreferClaude is true when a fresh snapshot shows headroom in both the
// 5-hour and weekly windows. When false, routing should prefer a
// non-claude fallback harness.
PreferClaude bool
// SnapshotPresent is true when a snapshot was found in the cache (even
// if stale).
SnapshotPresent bool
// Fresh is true when the snapshot is present and newer than staleAfter.
Fresh bool
// Age is the age of the snapshot relative to now (zero when absent).
Age time.Duration
// Snapshot is the cached snapshot when present.
Snapshot *ClaudeQuotaSnapshot
// Reason describes why the decision was made (diagnostic surface).
Reason string
}
ClaudeQuotaRoutingDecision summarises what foreground routing should do given the current cached snapshot.
func DecideClaudeQuotaRouting ¶
func DecideClaudeQuotaRouting(snapshot *ClaudeQuotaSnapshot, now time.Time, staleAfter time.Duration) ClaudeQuotaRoutingDecision
DecideClaudeQuotaRouting turns a cached snapshot into a routing decision for foreground callers. When the snapshot is missing or stale, the safe default is NOT to prefer claude (assume limited).
A snapshot counts as "limited" when either window reports zero or negative remaining headroom.
func ReadClaudeQuotaRoutingDecision ¶
func ReadClaudeQuotaRoutingDecision(now time.Time, staleAfter time.Duration) ClaudeQuotaRoutingDecision
ReadClaudeQuotaRoutingDecision is a convenience wrapper that reads the default cache and produces a routing decision in one call. It is the entry point foreground routing should use instead of any inline PTY capture.
type ClaudeQuotaSnapshot ¶
type ClaudeQuotaSnapshot struct {
CapturedAt time.Time `json:"captured_at"`
FiveHourRemaining int `json:"five_hour_remaining"`
FiveHourLimit int `json:"five_hour_limit"`
WeeklyRemaining int `json:"weekly_remaining"`
WeeklyLimit int `json:"weekly_limit"`
Windows []harnesses.QuotaWindow `json:"windows,omitempty"`
Source string `json:"source"` // e.g. "pty", "heuristic"
Account *harnesses.AccountInfo `json:"account,omitempty"`
}
ClaudeQuotaSnapshot captures Claude's current-quota headroom as absolute token/message counts. It is written to a durable per-user cache by an asynchronous capture path and read by foreground routing consumers.
The snapshot is intentionally distinct from the percentage-based QuotaSignal: foreground routing needs concrete numbers to reason about 5-hour / weekly headroom without invoking PTY capture inline.
func ReadClaudeQuota ¶
func ReadClaudeQuota() (*ClaudeQuotaSnapshot, bool)
The second return value is false if no snapshot is present or cannot be decoded.
Callers SHOULD check snapshot age via ClaudeQuotaSnapshotAge (or IsClaudeQuotaFresh) before trusting the values; this function does not itself enforce a TTL so that callers can report stale snapshots in diagnostic surfaces like `ddx agent doctor --routing`.
func ReadClaudeQuotaFrom ¶
func ReadClaudeQuotaFrom(path string) (*ClaudeQuotaSnapshot, bool)
ReadClaudeQuotaFrom reads the snapshot at the given path. Returns (nil, false) if the file does not exist or cannot be decoded. Non- existence is NOT an error: foreground callers are expected to fall back to a safe default when no snapshot is present.
func RefreshClaudeQuotaViaPTY ¶
func RefreshClaudeQuotaViaPTY(timeout time.Duration, opts ...QuotaPTYOption) (ClaudeQuotaSnapshot, error)
type QuotaPTYOption ¶
type QuotaPTYOption func(*quotaPTYOptions)
func WithQuotaPTYCassetteDir ¶
func WithQuotaPTYCassetteDir(dir string) QuotaPTYOption
func WithQuotaPTYCommand ¶
func WithQuotaPTYCommand(binary string, args ...string) QuotaPTYOption
func WithQuotaPTYEnv ¶
func WithQuotaPTYEnv(env ...string) QuotaPTYOption
func WithQuotaPTYWorkdir ¶
func WithQuotaPTYWorkdir(workdir string) QuotaPTYOption
type Runner ¶
type Runner struct {
// Binary is the absolute path to the claude executable. When empty the
// runner resolves "claude" via PATH at Execute time.
Binary string
// BaseArgs is prepended to the per-request argument list; callers use
// it to pin a consistent invocation profile (e.g. ["--print", "-p",
// "--output-format", "stream-json", "--verbose"]).
BaseArgs []string
// PromptMode controls how the prompt is delivered to claude:
// "stdin" (default) — prompt is piped on stdin
// "arg" — prompt is appended as the final positional argument
PromptMode string
// EventBuffer overrides the per-Execute channel buffer size. Zero
// selects defaultEventBuffer.
EventBuffer int
}
Runner is the subprocess-backed claude harness. It launches the claude CLI in stream-json mode, parses each line into harness Events, and emits a final Event when the subprocess exits. On ctx.Done(), the subprocess (and any forked children belonging to its process group) is signalled SIGTERM and reaped so PTY/tool children don't outlive the request.
func (*Runner) Execute ¶
func (r *Runner) Execute(ctx context.Context, req harnesses.ExecuteRequest) (<-chan harnesses.Event, error)
Execute runs one resolved request through the claude CLI and emits stream-derived events on the returned channel. The channel is closed once a final event has been emitted. PTY/orphan children are reaped on ctx.Done(). If the CLI rejects stream-json flags (older build), the runner falls back to a buffered legacy invocation and emits a single text_delta + final from the buffered output.
func (*Runner) HealthCheck ¶
HealthCheck verifies the claude binary resolves on PATH (or at the configured Binary). It does NOT invoke the binary so it stays cheap and safe to call from request hot paths. A future extension can probe quota state via the cache layer.
func (*Runner) Info ¶
func (r *Runner) Info() harnesses.HarnessInfo
Info returns identity + capability metadata for this harness.
Path is best-effort: the runner reports Binary if set, otherwise looks up "claude" on PATH. Available tracks whether the lookup succeeded so callers can show a useful error in `ddx agent list` without invoking HealthCheck synchronously.