/** * Settings declared by this domain (see `config/registry.ts`). Declaration order is the * settings-panel order; `config/all-settings.ts` registers every domain. */ import { type OAuthAccountPools, validateAgentAccountPools } from "../config/account-pools"; import { type AgentCompactionThresholdOverride, validateAgentCompactionThresholdOverrides, } from "../config/compaction-threshold"; import { effect, register } from "../config/registry"; import type { Settings } from "../config/settings"; import { type ServiceTierInheritSettingValue, validateAgentServiceTierOverrides } from "../config/service-tier"; import { THINKING_EFFORTS } from "@oh-my-pi/pi-catalog/effort"; import { $which, isRecord, logger, setWorktreesDir } from "@oh-my-pi/pi-utils"; import { setFeedModelBadgeEnabled } from "@oh-my-pi/pi-tui/render/render-utils"; import { getThinkingLevelMetadata } from "@oh-my-pi/pi-tui/thinking"; const EMPTY_AGENT_SERVICE_TIER_OVERRIDES: Record = {}; const EMPTY_AGENT_COMPACTION_THRESHOLD_OVERRIDES: Record = {}; const EMPTY_AGENT_ACCOUNT_POOLS: Record = {}; const EMPTY_AGENT_SOFT_REQUEST_BUDGET: Record = {}; const DEFAULT_AGENT_MODEL_OVERRIDES: Record = {}; // Delegation. Task and isolation settings declare `protocolDefault`: protocol hosts get neutral // defaults instead of the local user's interactive preferences. export const cfgTaskIsolationEnabled = register({ id: "task.isolation.enabled", protocolDefault: ["rpc", "acp"], type: "boolean", default: false, ui: { tab: "tasks", group: "Isolation", label: "Allow Isolated Subagents", description: "Allow per-task isolation with `isolated: true`; tasks without it run in the shared checkout", }, }); export const cfgIsolationBackend = register({ id: "isolation.backend", protocolDefault: ["rpc", "acp"], type: "enum", values: ["auto", "apfs", "btrfs", "zfs", "reflink", "overlayfs", "projfs", "block-clone", "rcopy"] as const, default: "auto", ui: { tab: "tasks", group: "Isolation", label: "Isolation Backend", description: "Backend used for subagent isolation and worktree cloning", options: [ { value: "auto", label: "Auto", description: "Isolated subagents use ovnfs checkouts when ovnfs is found; otherwise (and for worktrees) the PAL picks the best available backend", }, { value: "apfs", label: "APFS", description: "macOS clonefile reflink (APFS)" }, { value: "btrfs", label: "btrfs", description: "btrfs subvolume snapshot" }, { value: "zfs", label: "ZFS", description: "ZFS snapshot + clone" }, { value: "reflink", label: "Reflink", description: "Linux FICLONE per-file reflink" }, { value: "overlayfs", label: "Overlayfs", description: "Linux kernel overlay (or fuse-overlayfs fallback)", }, { value: "projfs", label: "ProjFS", description: "Windows Projected File System" }, { value: "block-clone", label: "Block clone", description: "Windows FSCTL_DUPLICATE_EXTENTS_TO_FILE (NTFS/ReFS)", }, { value: "rcopy", label: "Recursive copy", description: "git worktree if available, otherwise recursive copy", }, ], }, }); export const cfgWorktreeClone = register({ id: "worktree.clone", protocolDefault: ["rpc", "acp"], type: "boolean", default: true, ui: { tab: "tasks", group: "Isolation", label: "Clone Checkout into Worktrees", description: "New worktrees from `github pr_checkout` and `git worktree add` in bash start as a copy-on-write clone of the current checkout so ignored build artifacts (node_modules, target) carry over; falls back to a plain checkout when the filesystem cannot clone", }, }); export const cfgWorktreeCleanSource = register({ id: "worktree.cleanSource", protocolDefault: ["rpc", "acp"], type: "boolean", default: false, ui: { tab: "tasks", group: "Isolation", label: "Clean Source Checkout on /wt", description: "When creating a worktree with `/wt`, reset tracked changes and remove untracked files from the original checkout after carrying them over", }, }); export const cfgWorktreeOnStart = register({ id: "worktree.onStart", protocolDefault: ["rpc", "acp"], type: "enum", values: ["off", "ask", "create"] as const, default: "off", ui: { tab: "tasks", group: "Isolation", label: "Worktree on Start", description: "Whether fresh interactive sessions start in a new linked worktree", options: [ { value: "off", label: "Off", description: "Start in the current checkout" }, { value: "ask", label: "Ask", description: "Ask at session start" }, { value: "create", label: "Create", description: "Always start in a new worktree on a `wt/*` branch" }, ], }, }); export const cfgWorktreeOnExit = register({ id: "worktree.onExit", protocolDefault: ["rpc", "acp"], type: "enum", values: ["keep", "ask", "remove"] as const, default: "keep", ui: { tab: "tasks", group: "Isolation", label: "Worktree on Exit", description: "What to do on exit with worktrees created since omp started (on start or with `/wt`)", options: [ { value: "keep", label: "Keep", description: "Leave worktrees in place" }, { value: "ask", label: "Ask", description: "Ask on exit" }, { value: "remove", label: "Remove", description: "Remove clean worktrees; ask when they have uncommitted changes or new commits. Gitignored files (e.g. `.env`) are deleted too", }, ], }, }); export const cfgTaskIsolationApply = register({ id: "task.isolation.apply", protocolDefault: ["rpc", "acp"], type: "boolean", default: true, ui: { tab: "tasks", group: "Isolation", label: "Apply Isolated Changes", description: "Automatically apply successful isolated task changes to the parent checkout; disable to retain patch or branch artifacts", }, }); export const cfgOvnfsPath = register({ id: "ovnfs.path", type: "string", default: undefined, ui: { tab: "tasks", group: "Isolation", label: "ovnfs Binary", description: "ovnfs executable; unset looks for `ovnfs` on PATH. When it is found, ovnfs checkouts (copy-on-write views of a folder, ready in about a second at any size) serve every separate copy: the system prompt tells agents to open checkouts instead of git worktrees or copies, and isolated subagents (when allowed, with `isolation.backend` on auto) run in checkouts instead of native clones", }, }); /** * The ovnfs command agents and isolated subagents use (`ovnfs.path`, else `ovnfs`), * or `undefined` when that binary is not found. Read by the system prompt (checkout * guidance), the bash git guard, `grep`/`glob` (searches through the daemon), and the * task runner (ovnfs-backed isolation when `isolation.backend` is auto). */ export function ovnfsCommand(settings: Settings): string | undefined { const command = cfgOvnfsPath.get(settings)?.trim() || "ovnfs"; return $which(command) ? command : undefined; } export const cfgTaskIsolationMerge = register({ id: "task.isolation.merge", protocolDefault: ["rpc", "acp"], type: "enum", values: ["patch", "branch"] as const, default: "patch", ui: { tab: "tasks", group: "Isolation", label: "Isolation Merge Strategy", description: "How isolated task changes are integrated (patch apply or branch merge)", options: [ { value: "patch", label: "Patch", description: "Combine diffs and git apply" }, { value: "branch", label: "Branch", description: "Commit per task, merge with --no-ff" }, ], }, }); export const cfgTaskIsolationCommits = register({ id: "task.isolation.commits", protocolDefault: ["rpc", "acp"], type: "enum", values: ["generic", "ai"] as const, default: "generic", ui: { tab: "tasks", group: "Isolation", label: "Isolation Commit Style", description: "Commit message style for nested repo changes (generic or AI-generated)", options: [ { value: "generic", label: "Generic", description: "Static commit message" }, { value: "ai", label: "AI", description: "AI-generated commit message from diff" }, ], }, }); export const cfgWorktreeBase = register({ id: "worktree.base", type: "string", default: undefined, ui: { tab: "tasks", group: "Isolation", label: "Worktree Base Directory", description: "Base directory for agent-managed worktrees — task-isolation copies, `github` PR checkouts, and `omp worktree` cleanup all live here. Unset uses ~/.omp/wt. Must be an absolute or ~-relative path; relative paths are ignored. The OMP_WORKTREE_DIR env var overrides this.", }, }); effect(cfgWorktreeBase, value => { const dir = value?.trim() ? value : undefined; // Always applied, so an unset/empty value clears a previous override. setWorktreesDir expands `~`, // rejects relative paths, and returns the applied absolute path (undefined when cleared/rejected). if (!setWorktreesDir(dir) && dir) { logger.warn("Settings: worktree.base must be an absolute or ~-relative path; ignoring", { value: dir }); } }); export const cfgTaskEager = register({ id: "task.eager", protocolDefault: ["rpc", "acp"], type: "enum", values: ["default", "preferred", "always"] as const, default: "default", ui: { tab: "tasks", group: "Subagents", label: "Prefer Task Delegation", description: "How strongly to push the main agent to delegate work; subagents keep the model's own policy", options: [ { value: "default", label: "Default", description: "Uses the selected model's policy; some models require an explicit delegation request", }, { value: "preferred", label: "Preferred", description: "Adds delegation guidance to the system prompt" }, { value: "always", label: "Always", description: "Prompt guidance plus a first-turn delegation reminder" }, ], }, }); export const cfgTaskBatch = register({ id: "task.batch", protocolDefault: ["rpc", "acp"], type: "boolean", default: true, ui: { tab: "tasks", group: "Subagents", label: "Batch Task Calls", description: "Switch the task tool to its batch shape: one call carries { context, tasks[] } — one subagent per item, with an optional per-item agent (defaulting to the session spawn-policy agent), per-item isolation, and a required shared context prepended to every assignment. With async.enabled=true, each spawn runs as an independent background agent with the normal idle/parked lifecycle; otherwise the call blocks for merged results. Disable to restore the flat single-spawn schema.", }, }); export const cfgTaskFile = register({ id: "task.file", type: "boolean", default: false, ui: { tab: "tasks", group: "Subagents", label: "Task Instructions from Files", description: "Let the task tool read subagent instructions from a file (taskFile), recording its path and SHA-256.", }, }); export const cfgTaskStructured = register({ id: "task.structured", type: "boolean", default: false, ui: { tab: "tasks", group: "Subagents", label: "Structured Task Results", description: "Expose outputSchema/schemaMode on the task tool and return subagent results through the yield tool, validated against caller or agent output schemas. Off: task subagents have no yield tool and return their final message as plain text, except agents whose definition sets structured: true. Eval agent()/workpool spawns always keep structured results.", }, }); export const cfgTaskSpeculativeLaunch = register({ id: "task.speculativeLaunch", type: "boolean", default: true, ui: { tab: "tasks", group: "Subagents", label: "Speculative Task Launch", description: "Start each batch subagent as soon as its tasks[] item finishes streaming instead of waiting for the whole task call. Launched agents are aborted if the finished call fails validation, is blocked, or its arguments change. Requires auto-allowed task approval and no extension tool lifecycle handlers.", }, }); export const cfgTaskEnableEffort = register({ id: "task.enableEffort", type: "boolean", default: false, ui: { tab: "tasks", group: "Subagents", label: "Per-Task Effort", description: "Expose the optional effort parameter on task spawns, allowing callers to override each subagent's thinking level", }, }); export const cfgTaskPerCallModel = register({ id: "task.perCallModel", type: "enum", values: ["off", "eval", "on"] as const, default: "off", ui: { tab: "tasks", group: "Subagents", label: "Per-Call Model", description: "Whether a subagent call may pick its model with one exact provider/model-id[@upstream][:level] selector. An @upstream pin is part of the model identity and is supported only by aggregators. Only models the configuration already routes to are accepted. Project settings can turn it off but not on, and never approve a model.", options: [ { value: "off", label: "Off", description: "Reject `model` on task items and eval agent()/workpool()" }, { value: "eval", label: "Eval only", description: "Accept `model` in eval agent() and workpool() only" }, { value: "on", label: "On", description: "Accept `model` on task items and in eval agent()/workpool()" }, ], }, }); /** * Whether `task.perCallModel` accepts a per-call `model` for this invocation kind. The operator's * layers ({@link Settings.grantAuthority}) must enable it; project and subagent-injected settings, * which the live merged value includes, can only turn it off. */ export function perCallModelEnabled(settings: Settings, invocationKind: "task" | "eval"): boolean { return [settings.grantAuthority(), settings].every(layers => { const mode = cfgTaskPerCallModel.get(layers); return mode === "on" || (mode === "eval" && invocationKind === "eval"); }); } /** * Validates `task.perCallModelAllow`: a list of exact `provider/model-id` strings. A non-string * entry fails load and writes instead of reaching approval as an entry that matches nothing. */ function validatePerCallModelAllow(value: unknown): void { if (value === undefined || value === null) return; if (!Array.isArray(value)) { throw new Error( `Invalid task.perCallModelAllow: expected a list of provider/model-id strings, got ${Bun.inspect(value)}.`, ); } for (const [index, entry] of value.entries()) { if (typeof entry === "string") continue; throw new Error( `Invalid task.perCallModelAllow[${index}]: expected a provider/model-id string, got ${Bun.inspect(entry)}.`, ); } } export const cfgTaskPerCallModelAllow = register({ id: "task.perCallModelAllow", type: "array", default: [] as string[], validate: validatePerCallModelAllow, ui: { tab: "tasks", group: "Subagents", label: "Per-Call Model Allowlist", editor: "json", // The editor writes the global layer; hide a project-supplied list so the repository cannot promote it. condition: "perCallModelAllowNotProject", description: "Exact selectors: `provider/model-id`, including `@upstream` only for a pinned OpenRouter/Vercel route (e.g. `openrouter/x@y`). When set, a per-call model must also be listed here. Empty: every model the configuration grants. A project list can only narrow this one.", }, }); export const cfgTaskMaxConcurrency = register({ id: "task.maxConcurrency", protocolDefault: ["rpc", "acp"], type: "number", default: 32, ui: { tab: "tasks", group: "Subagents", label: "Max Concurrent Tasks", description: "Maximum number of subagents running concurrently", options: [ { value: "0", label: "Unlimited" }, { value: "1", label: "1 task" }, { value: "2", label: "2 tasks" }, { value: "4", label: "4 tasks" }, { value: "8", label: "8 tasks" }, { value: "16", label: "16 tasks" }, { value: "32", label: "32 tasks" }, { value: "64", label: "64 tasks" }, ], }, }); function validateWorkerThreads(value: unknown): void { if (value === undefined || (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)) return; throw new Error( `Invalid task.workerThreads: expected a non-negative integer (0 runs subagents on the main thread), got ${Bun.inspect(value)}.`, ); } /** * Upper bound on worker threads hosting subagents; how many are used follows the number of * concurrent subagents (`allowedSubagentWorkers`). 0 keeps every subagent on the main thread. */ export const cfgTaskWorkerThreads = register({ id: "task.workerThreads", protocolDefault: ["rpc", "acp"], type: "number", default: 4, validate: validateWorkerThreads, ui: { tab: "tasks", group: "Subagents", label: "Subagent Worker Threads", description: "Maximum worker threads for subagents. Up to 8 concurrent subagents run on the main thread; at 9 one worker joins, and one more each time the count doubles (18, 36, 72). Workers stop a minute after no subagent is live in them; 0 keeps every subagent on the main thread", options: [ { value: "0", label: "Main thread only" }, { value: "1", label: "Up to 1 worker" }, { value: "2", label: "Up to 2 workers" }, { value: "4", label: "Up to 4 workers", description: "Default" }, { value: "8", label: "Up to 8 workers" }, ], }, }); export const cfgTaskEnableLsp = register({ id: "task.enableLsp", type: "boolean", default: false, ui: { tab: "tasks", group: "Subagents", label: "LSP in Subagents", description: "Allow subagents spawned via the task tool to use the lsp tool. Off by default to keep subagents cheap; enable when LSP-aware delegation is worth the extra tokens.", }, }); export const cfgTaskMaxRecursionDepth = register({ id: "task.maxRecursionDepth", protocolDefault: ["rpc", "acp"], type: "number", default: 2, ui: { tab: "tasks", group: "Subagents", label: "Max Task Recursion", description: "How many levels deep subagents can spawn their own subagents", options: [ { value: "-1", label: "Unlimited" }, { value: "0", label: "None" }, { value: "1", label: "Single" }, { value: "2", label: "Double" }, { value: "3", label: "Triple" }, ], }, }); export const cfgTaskMaxRuntimeMs = register({ id: "task.maxRuntimeMs", type: "number", default: 0, ui: { tab: "tasks", group: "Subagents", label: "Max Subagent Runtime", description: "Hard wall-clock limit per subagent (ms). 0 disables it. Defense-in-depth against provider-side stream hangs that escape the inference-layer watchdog; triggers a normal subagent abort with a 'timed out' reason.", options: [ { value: "0", label: "Unlimited", description: "Default" }, { value: "300000", label: "5 minutes" }, { value: "900000", label: "15 minutes" }, { value: "1800000", label: "30 minutes" }, { value: "3600000", label: "1 hour" }, ], }, }); export const cfgTaskCompletionProbe = register({ id: "task.completionProbe", type: "boolean", default: true, ui: { tab: "tasks", group: "Subagents", label: "Subagent Completion Probe", description: "Ask a working subagent, through a cached side request like /btw, to estimate how complete its task is: after 2, 5, 10 and 30 more minutes, then hourly. The estimate shows next to the subagent in wait and task views. Only subagents spawned by the main agent of an interactive session are asked; print, RPC, ACP and SDK runs never probe.", }, }); export const cfgTaskAgentIdleTtlMs = register({ id: "task.agentIdleTtlMs", type: "number", default: 420_000, ui: { tab: "tasks", group: "Subagents", label: "Agent Idle TTL", description: "How long an idle subagent stays live in memory before being parked to disk (ms). Parked agents are revived automatically when messaged or resumed. 0 keeps idle agents live until exit.", }, }); export const cfgTaskSoftRequestBudget = register({ id: "task.softRequestBudget", type: "number", default: 200, ui: { tab: "tasks", group: "Subagents", label: "Soft Subagent Request Budget", description: "Soft per-subagent request budget (assistant requests per run). Crossing it injects a wrap-up steering notice (see task.softRequestBudgetNotice); at 1.5x the budget the run is force-stopped and the agent must hand back its partial findings. 0 disables the guard. Bundled scout/sonic agents cap out at a lower built-in budget, so a value below that cap still applies to them. A task.agentSoftRequestBudget entry replaces this budget for the agent it names.", options: [ { value: "0", label: "Disabled" }, { value: "90", label: "90 requests" }, { value: "150", label: "150 requests" }, { value: "200", label: "200 requests", description: "Default" }, ], }, }); /** * Validates `task.agentSoftRequestBudget`: exact agent name → request budget, * a non-negative integer (0 disables that agent's guard). A `null` entry clears * one inherited from a lower-priority settings layer. */ function validateAgentSoftRequestBudget(value: unknown): void { if (value === undefined || value === null) return; if (!isRecord(value)) { const received = Array.isArray(value) ? "an array" : `a ${typeof value}`; throw new Error( `Invalid task.agentSoftRequestBudget: expected a map of agent name to request budget, got ${received}.`, ); } for (const [agentName, budget] of Object.entries(value)) { if (budget === null || (typeof budget === "number" && Number.isSafeInteger(budget) && budget >= 0)) continue; throw new Error( `Invalid task.agentSoftRequestBudget.${agentName}: expected a non-negative integer request count (0 disables the guard), got ${Bun.inspect(budget)}.`, ); } } export const cfgTaskAgentSoftRequestBudget = register({ id: "task.agentSoftRequestBudget", type: "record", default: EMPTY_AGENT_SOFT_REQUEST_BUDGET, validate: validateAgentSoftRequestBudget, }); export const cfgTaskSoftRequestBudgetNotice = register({ id: "task.softRequestBudgetNotice", type: "boolean", default: true, ui: { tab: "tasks", group: "Subagents", label: "Soft Request Budget Notice", description: "Inject one steering notice when a subagent crosses its soft request budget, asking it to wrap up before the 1.5x forced-yield stop.", }, }); export const cfgTaskMaxEffort = register({ id: "task.maxEffort", type: "enum", values: THINKING_EFFORTS, default: "max", ui: { tab: "tasks", group: "Subagents", label: "Maximum Per-Spawn Effort", description: "Maximum reasoning effort allowed for the task tool's per-spawn effort hint or a per-call model. An explicit `:level` is clamped to the highest supported effort at or below this ceiling, and an omitted `:level` inherits the agent's thinking level capped at this value; cold revival re-applies the cap. Unset, `:inherit` and `:off` keep their existing meaning. A per-call model must support an effort at or below this ceiling; the default preserves the model's full range.", options: THINKING_EFFORTS.map(getThinkingLevelMetadata), }, }); export const cfgTaskDisabledAgents = register({ id: "task.disabledAgents", protocolDefault: ["rpc", "acp"], type: "array", default: [] as string[], }); export const cfgTaskAgentModelOverrides = register({ id: "task.agentModelOverrides", protocolDefault: ["rpc", "acp"], type: "record", default: DEFAULT_AGENT_MODEL_OVERRIDES, }); export const cfgTaskAgentServiceTierOverrides = register({ id: "task.agentServiceTierOverrides", protocolDefault: ["rpc", "acp"], type: "record", default: EMPTY_AGENT_SERVICE_TIER_OVERRIDES, validate: validateAgentServiceTierOverrides, }); export const cfgTaskAgentCompactionThresholdOverrides = register({ id: "task.agentCompactionThresholdOverrides", protocolDefault: ["rpc", "acp"], type: "record", default: EMPTY_AGENT_COMPACTION_THRESHOLD_OVERRIDES, validate: validateAgentCompactionThresholdOverrides, }); export const cfgTaskAgentAccountPools = register({ id: "task.agentAccountPools", protocolDefault: ["rpc", "acp"], type: "record", default: EMPTY_AGENT_ACCOUNT_POOLS, validate: validateAgentAccountPools, }); export const cfgTaskAgentPrewalk = register({ id: "task.agentPrewalk", protocolDefault: ["rpc", "acp"], type: "record", default: {} as Record, }); export const cfgTaskAgentAdvisor = register({ id: "task.agentAdvisor", protocolDefault: ["rpc", "acp"], type: "record", default: {} as Record, }); export const cfgTaskPrewalk = register({ id: "task.prewalk", type: "boolean", default: false, ui: { tab: "tasks", group: "Subagents", label: "Generic Task Prewalk", description: "Arm prewalk for the bundled generic `task` subagent: it starts on its resolved model, plans and begins the implementation, then hands off to the 'smol' role at its first edit/write. Per-agent overrides (task.agentPrewalk, configured from the /agents hub) and user agent `prewalk` frontmatter apply regardless of this toggle.", }, }); export const cfgTaskShowResolvedModelBadge = register({ id: "task.showResolvedModelBadge", type: "boolean", default: false, ui: { tab: "appearance", group: "Display", label: "Show Resolved Model Badge", description: "Display the actual model ID used by each subagent in the task widget status line", }, }); effect(cfgTaskShowResolvedModelBadge, setFeedModelBadgeEnabled);