Configuration
September 10, 2026 · View on GitHub
billion-context-pi works out of the box with no configuration — it reads your model's context window automatically and applies sensible defaults. This document is the complete reference for the optional JSON configuration file (acp.json) and the environment variables that let you tune behavior.
Configuration is layered: environment variables take the highest precedence, followed by the project config file, then the global config file, and finally the built-in defaults.
Config file locations
Settings are read from JSON files named acp.json. The global file applies to every project; a project file overrides the global one on a per-field basis (individual keys you do not set in the project file still fall back to the global value).
| Scope | Path | Applies to |
|---|---|---|
| Global | ~/.pi/acp.json | All projects on this machine |
| Project | <project>/.pi/acp.json | The current project only (overrides global per-field) |
Precedence: Environment variable > Project file > Global file > Built-in default.
Files are loaded at session start. Missing files, malformed JSON, and unknown keys are silently ignored — the extension never fails to start because of a config issue. Only the documented keys are read; everything else is discarded.
Quick start
Create ~/.pi/acp.json (or <project>/.pi/acp.json) and drop in whichever keys you want to change. Every field below is optional — omit a key to keep its default.
{
"debug": false,
"autoUpdate": true,
"modelContextLimit": 200000,
"outputHeadroomMaxPct": 0.25,
"toolBashDefaultTimeout": 60,
"toolOutputMaxBytes": 200000,
"throttleRetry": {
"enabled": true,
"maxRetries": 10
},
"delegate": {
"enabled": true,
"displayUsage": "separate"
},
"compress": {
"maxContextLimit": "75%",
"emergencyThresholdPercent": "95%",
"nudgeGrowthTokens": 50000,
"reasoning": { "drop": true, "threshold": 2048 }
}
}
A minimal config enabling only debug logging:
{
"debug": true
}
An advanced config overriding the kernel's compression prompt rules (requires the risk acknowledgement). Set only the fields you want to change; the rest inherit the kernel defaults:
{
"prompts": {
"compressPhilosophy": "My compression philosophy...",
"howToCompressRules": "My tier-1 rules...",
"tier2DistillRules": "My tier-2 distillation rules...",
"tier3CondenseRules": "My tier-3 condensation rules..."
},
"acknowledgePromptsRisk": true
}
Parameter Reference
Status legend
| Status | Meaning |
|---|---|
| 🟢 ACTIVE | Fully supported, documented, and recommended for use. |
All keys below are currently ACTIVE.
Summary
Top-level keys
| Key | Type | Default | Status | Description |
|---|---|---|---|---|
enabled | boolean | true | 🟢 ACTIVE | Master switch. false turns the whole adapter off (no tools, no system prompt, no context transform) — for models too small to handle ACP. Requires a Pi restart. |
debug | boolean | false | 🟢 ACTIVE | Enable verbose debug-level events in the log. |
autoUpdate | boolean | true | 🟢 ACTIVE | Check npm for a newer version on startup and auto-install it. |
modelContextLimit | number | (auto) | 🟢 ACTIVE | Override the context limit (in tokens). |
outputHeadroomMaxPct | number | string | 0.25 | 🟢 ACTIVE | Cap on the output-headroom reservation, as a fraction of the context window. |
toolBashDefaultTimeout | number | 60 | 🟢 ACTIVE | Default bash tool timeout in seconds when the model omits it. |
toolOutputMaxBytes | number | 200000 | 🟢 ACTIVE | Hard byte cap on tool result text. |
throttleRetry | boolean | object | true | 🟢 ACTIVE | Auto-retry provider token rate-limit errors with progressive backoff. |
repetitionGuard | boolean | object | true | 🟢 ACTIVE | Break infinite loops of byte-identical tool calls (warn at 3 consecutive, block + abort at 5). |
degenerationGuard | boolean | object | true | 🟢 ACTIVE | Collapse degenerate single-codepoint runs (e.g. 4655×「【」) in assistant text/thinking of the outgoing view and inject a one-shot recovery notice — breaks the abort loop where pi replays degenerated thinking back to the provider on every request (#351). |
Delegate keys
| Key | Type | Default | Status | Description |
|---|---|---|---|---|
delegate.enabled | boolean | true | 🟢 ACTIVE | Enable the acp_delegate tools and their system-prompt section. |
delegate.displayUsage | string | "separate" | 🟢 ACTIVE | Controls how delegate sub-agent token usage is reported. |
delegate.maxDepth | number | 2 | 🟢 ACTIVE | Max nesting depth for acp_delegate (main session = depth 0; a session at this depth is a leaf and cannot delegate again). Set 1 so delegates never nest. |
delegate.syncTimeoutMinutes | number | 5 | 🟢 ACTIVE | Hard timeout for synchronous acp_delegate calls, in minutes. 0 / null disables it. |
delegate.idleTimeoutMinutes | number | 5 | 🟢 ACTIVE | Idle watchdog for async delegate children — force-finish after this many minutes without output. 0 / null disables it. |
delegate.asyncTimeoutMinutes | number | 30 | 🟢 ACTIVE | Absolute hard limit for async delegate children, in minutes. 0 / null disables it. |
delegate.maxConcurrent | number | unlimited | 🟢 ACTIVE | Max background (async) delegates running at once; extra launches queue FIFO and start as slots free. 1 = forced serial. Overridden by PI_ACP_DELEGATE_MAX_CONCURRENT. |
delegate.thinkingLevel | string | (unset) | 🟢 ACTIVE | Global default thinking level for delegates (per-call > role > global > Pi default). |
delegate.agents | object | (unset) | 🟢 ACTIVE | Per-role default model + thinking level, keyed by role name. |
Provider throttle retry keys
| Key | Type | Default | Status | Description |
|---|---|---|---|---|
throttleRetry.enabled | boolean | true | 🟢 ACTIVE | Enable auto-retry of provider token rate-limit errors. |
throttleRetry.maxRetries | number | 10 | 🟢 ACTIVE | Total budget of ACP-driven retries per error episode. |
throttleRetry.baseDelayMs | number | 60000 | 🟢 ACTIVE | Delay before the first paced kick. |
throttleRetry.maxDelayMs | number | 300000 | 🟢 ACTIVE | Cap for paced kick delays. |
throttleRetry.backoffMode | string | "exponential" | 🟢 ACTIVE | Delay progression: "exponential"$ ( \times 2 \text{per} \text{kick}) \text{or} $"fixed". |
Repetition guard keys
| Key | Type | Default | Status | Description |
|---|---|---|---|---|
repetitionGuard.enabled | boolean | true | 🟢 ACTIVE | Enable the repetition breaker. false disables it entirely. |
repetitionGuard.warn | number | 3 | 🟢 ACTIVE | Consecutive byte-identical calls before a strong warning is appended to the tool result. |
repetitionGuard.abort | number | 5 | 🟢 ACTIVE | Consecutive byte-identical calls before the call is blocked (not executed) and the turn is aborted. Must exceed warn. |
degenerationGuard.enabled | boolean | true | 🟢 ACTIVE | Enable the degenerate-repeat guard. false disables it entirely. |
degenerationGuard.minRun | number | 200 | 🟢 ACTIVE | Minimum length of a single-codepoint run before it is treated as degeneration and collapsed. Values below 8 are raised to 8. |
Compression keys
| Key | Type | Default | Status | Description |
|---|---|---|---|---|
compress.maxContextLimit | number | string | "75%" | 🟢 ACTIVE | Context threshold that triggers forced compression nudges. |
compress.emergencyThresholdPercent | number | string | "95%" | 🟢 ACTIVE | Context threshold that triggers emergency truncation. |
compress.nudgeGrowthTokens | number | 50000 | 🟢 ACTIVE | Token growth step for soft compression nudges. |
compress.reasoning | object | { "drop": true, "threshold": 2048 } | 🟢 ACTIVE | Drop oversized thinking from historical compress calls (request-time; persisted history untouched). |
Prompts keys
| Key | Type | Default | Status | Description |
|---|---|---|---|---|
prompts | object | (kernel defaults) | 🟢 ACTIVE | Override acp-kernel's 4 load-bearing compression prompt rules. Each set field replaces the default verbatim. |
acknowledgePromptsRisk | boolean | false | 🟢 ACTIVE | Must be true for prompts overrides to take effect; otherwise overrides are dropped and defaults are used. |
Environment variables
| Variable | Effect |
|---|---|
ACP_AUTO_UPDATE | Set to 0 / false to disable auto-update (overrides autoUpdate). |
ACP_MODEL_CONTEXT_LIMIT | Override the context limit (takes highest precedence). |
ACP_DEBUG | Set to 1 / true to enable debug logging. |
ACP_LOG_FILE | Override the log file path (default ~/.pi/acp.log). |
PI_ACP_DELEGATE_MAX_DEPTH | Override delegate.maxDepth. |
PI_ACP_DELEGATE_SYNC_TIMEOUT_MINUTES | Override delegate.syncTimeoutMinutes; 0 disables the sync hard timeout. |
PI_ACP_DELEGATE_IDLE_TIMEOUT_MINUTES | Override delegate.idleTimeoutMinutes; 0 disables the idle watchdog. |
PI_ACP_DELEGATE_ASYNC_TIMEOUT_MINUTES | Override delegate.asyncTimeoutMinutes; 0 disables the async hard limit. |
Only the documented keys are read from
acp.json. Other tuning knobs (preserveRecentMessages,protectedTools) are code-level and not user-overridable. The three compression thresholds form a three-tier escalation: growth-driven soft nudges → forced nudges atcompress.maxContextLimit→ emergency truncation atcompress.emergencyThresholdPercent.
General
enabled
- Type:
boolean - Default:
true - Status: 🟢 ACTIVE
- Description: Master switch for the entire adapter. Set to
falseto turn ACP off completely: nocompress/decompress/search_context/acp_statustools, no ACP system prompt, no context transformation, and no suppression of Pi's built-in auto-compaction — Pi's native context management runs instead. Intended for small local models (e.g. quantized 27B) that cannot reliably drive ACP's compression loop. Checked once at extension load, so it requires restarting Pi after editingacp.json. Project-localacp.jsonoverrides the global one.
debug
- Type:
boolean - Default:
false - Status: 🟢 ACTIVE
- Description: Enable verbose debug-level events in the log file (default
~/.pi/acp.log). The always-on log (session/turn/compress/delegate lifecycle events, all errors and warnings) is written regardless of this setting;debugonly adds extra diagnostics such as full field dumps and per-turn internals. Also enabled by the environment variableACP_DEBUG=1(orACP_DEBUG=true).
autoUpdate
- Type:
boolean - Default:
true - Status: 🟢 ACTIVE
- Description: On Pi startup, check the npm registry for a newer version of
billion-context-piand auto-install it. Set tofalseto avoid all startup network calls. Can also be disabled via theACP_AUTO_UPDATEenvironment variable (ACP_AUTO_UPDATE=0orACP_AUTO_UPDATE=false), which overrides this setting.- Read-only install location: when the copy's install prefix is not writable (e.g. a root-owned
npm i -gglobal prefix), auto-update stops retrying that location after the firstEACCES/permission failure and shows a one-time hint to runnpm i -g billion-context-pi(or remove the global copy if you rely on pi's bundled install) instead of looping. The check throttle and the stop-retry marker are keyed per install location, so a healthy copy never suppresses a failing one's checks. - Two parallel mechanisms: this extension-side auto-update is independent of pi's own core update banner — both can appear, and disabling one does not disable the other.
- Read-only install location: when the copy's install prefix is not writable (e.g. a root-owned
modelContextLimit
- Type:
number - Default: (auto) — the model's
contextWindowread live each turn - Status: 🟢 ACTIVE
- Description: Override the context limit, in tokens. By default the limit is read from the active model's
ctx.model.contextWindowon every turn, so it stays correct when you switch models. Set an explicit value for deterministic test runs or headless/non-interactive sessions where the model metadata may be unavailable. TheACP_MODEL_CONTEXT_LIMITenvironment variable takes precedence over this value.
outputHeadroomMaxPct
- Type:
number | string(ratio or percent string) - Default:
0.25 - Status: 🟢 ACTIVE
- Description: Caps the output-headroom reservation as a fraction of the context window: reserved = min(model.maxTokens, pct × window). The reservation keeps the kernel's nudge/truncate bands below (window − reserved) so a long reply cannot push input + output past the window on APIs that count output against the window (all except Anthropic Messages, which enforces its input limit independently and is exempt). Without a cap, models whose registered max output is a large share of the window (e.g. 131072 on a 262144 window) lose most of their input budget — the 75% force-compress band then fires at roughly a third of the full window. The 0.25 default bounds that loss while still guaranteeing any single-turn reply up to 25% of the window fits at the 95% emergency threshold; longer replies overflow once and are recovered by the overflow self-heal on the next turn. Accepts a ratio (
0.25) or percent string ("25%"). Set0to disable the reservation entirely;1(or greater) restores the legacy full-capability reservation.
toolBashDefaultTimeout
- Type:
number - Default:
60 - Status: 🟢 ACTIVE
- Description: The number of seconds injected into the
bashtool when the model omits thetimeoutparameter. Pi has no built-in default timeout of its own, so without this guard a command the model forgets to time out can hang for thousands of seconds. On timeout the model is guided to re-run the command with a largertimeout. Set to0to disable this guard and restore Pi's unbounded behavior.
toolOutputMaxBytes
- Type:
number - Default:
200000 - Status: 🟢 ACTIVE
- Description: A hard byte cap (~200 KB, roughly 5000 lines) applied to tool result text via the
tool_resulthook. It stops runaway output that Pi's own caps cannot catch (for example, from tools Pi does not cap). When the cap fires, the oversized text is head-truncated with a notice telling the model how to see the full output. Set lower (e.g.8192) for a tighter context budget, or set to0to disable the cap entirely.
Delegate
The delegate sub-object controls the acp_delegate sub-agent tool family (acp_delegate, acp_delegate_wait, acp_delegate_cancel) and how their token usage is reported.
Backward compatibility: For convenience,
delegateaccepts both an object and a boolean shorthand:
delegate: trueis treated asdelegate: { enabled: true }.- The legacy flat top-level
displayUsagekey is still accepted as an alias fordelegate.displayUsage. Prefer the nesteddelegate.displayUsageform.
delegate.enabled
- Type:
boolean - Default:
true - Status: 🟢 ACTIVE
- Description: Enable the
acp_delegatetools (acp_delegate,acp_delegate_wait,acp_delegate_cancel) and the system-prompt section that describes them. Set tofalseto skip registering them entirely — for example, if you use a different sub-agent extension, or when running headless where async result injection adds no value.
delegate.displayUsage
- Type: string enum
"merged" | "separate" - Default:
"separate" - Status: 🟢 ACTIVE
- Description: Controls how delegate sub-agent token usage is reported back to the main session.
"separate"(default) tracks delegate tokens in a separate accumulator — the main session totals stay clean and delegate usage shows as its own block inacp_status(excluded from main totals)."merged"folds delegate token usage into the tool-resultusagefield so it is counted as part of the main session totals. Only meaningful whendelegate.enabledistrue.
delegate.maxDepth
- Type: integer ≥ 1
- Default:
2 - Status: 🟢 ACTIVE
- Description: Maximum nesting depth for
acp_delegate. Depth counts how far a session sits below the main session (main = 0); a session may only spawn a delegate while its own depth is below this limit, so a session at the limit becomes a leaf and cannot delegate again. The default2allows main → delegate → sub-delegate; set1for an orchestrator / leaf-worker pattern where delegates never nest further. The resolved limit is propagated to children via the internalPI_ACP_DELEGATE_MAX_DEPTHenvironment variable, so it binds the whole delegation tree even if a child loads a different projectacp.json. Invalid values (non-integer,< 1) fall back to the default with a warning log. Environment override:PI_ACP_DELEGATE_MAX_DEPTH(takes precedence over this key).
delegate.syncTimeoutMinutes
- Type: number (minutes, fractional allowed) or
0/null - Default:
5 - Status: 🟢 ACTIVE
- Description: Hard timeout for synchronous
acp_delegatecalls — the child process is killed (SIGTERM) if it has not finished within this window. Set0(ornull) to run synchronous delegates without a hard timeout. Fractional minutes are accepted (e.g.0.5= 30s). Invalid values fall back to the default with a warning log. Environment override:PI_ACP_DELEGATE_SYNC_TIMEOUT_MINUTES(0disables).
delegate.idleTimeoutMinutes
- Type: number (minutes, fractional allowed) or
0/null - Default:
5 - Status: 🟢 ACTIVE
- Description: Idle watchdog for async delegate children: if a child produces no output for this long, it is considered hung and force-finished. This is the primary defense against a stuck child holding its stdout pipe open. Set
0(ornull) to disable it — ACP logs a prominent warning when you do;acp_delegate_cancelremains available as a manual escape hatch. Fractional minutes are accepted. Invalid values fall back to the default with a warning log. Environment override:PI_ACP_DELEGATE_IDLE_TIMEOUT_MINUTES(0disables).
delegate.asyncTimeoutMinutes
- Type: number (minutes, fractional allowed) or
0/null - Default:
30 - Status: 🟢 ACTIVE
- Description: Absolute hard limit for asynchronous delegate children, regardless of activity. Set
0(ornull) to run long tasks without an absolute cap — the idle watchdog still applies unless separately disabled. Fractional minutes are accepted. Invalid values fall back to the default with a warning log. Environment override:PI_ACP_DELEGATE_ASYNC_TIMEOUT_MINUTES(0disables).
delegate.maxConcurrent
- Type: number (integer ≥ 1)
- Default: unlimited (no concurrency cap)
- Status: 🟢 ACTIVE
- Environment override:
PI_ACP_DELEGATE_MAX_CONCURRENT(takes precedence over this key) - Description: Caps how many background (
async: true) delegates run at the same time. When the limit is reached, further launches are held in a FIFO queue and start automatically as soon as a slot frees, so nothing is dropped — they just wait their turn. Set1to force strictly serial execution (useful on low-power machines where parallel sub-agents contend for CPU and time out). Sync (async: false) calls always run immediately and are not affected by this cap. Invalid values (non-integers or< 1) fall back to unlimited with a warning rather than failing the session. Only meaningful whendelegate.enabledistrue.
delegate.thinkingLevel
- Type: string enum
"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" - Default: (unset — each child uses Pi's own default)
- Status: 🟢 ACTIVE
- Description: Global default thinking level applied to every delegate when neither the per-call
thinkingLevelnor the role's ownthinkingLevel(seedelegate.agents) is set. Without any value at all levels, no--thinkingflag is passed and each child runs on Pi's own default. An invalid value is ignored with a warning logged (it never fails the run). A per-callacp_delegate({ thinkingLevel })always wins over this global.
delegate.agents
- Type: object — map of role name →
{ model?, thinkingLevel? } - Default: (unset — all roles inherit the parent model + Pi defaults)
- Status: 🟢 ACTIVE
- Description: Per-role defaults so long-lived automation can pin a cheaper or more capable model and thinking level per delegate role without the main agent having to fill them in on every call. Keys are role names (
reviewer,researcher,worker,planner,oracle, or any custom role). Each value may set:model("provider/id") — this role's default model. Resolution priority: per-callmodel> this role'smodel> parent agent's current model. A value that isn't a valid"provider/id"is ignored. If the configured model doesn't exist in the live registry, the child falls back to the parent model and a warning is logged — it never fails.thinkingLevel— this role's default thinking level (same enum asdelegate.thinkingLevel). Priority: per-call > role > global.
{
"delegate": {
"thinkingLevel": "low",
"agents": {
"reviewer": { "model": "opencode-go/deepseek-v4-flash", "thinkingLevel": "high" },
"worker": { "model": "anthropic/claude-sonnet-4-5" },
"oracle": { "model": "openai/gpt-5", "thinkingLevel": "xhigh" }
}
}
}
Provider Throttle Retry
The throttleRetry key controls auto-retry of provider-side token rate-limit errors — e.g. AWS Bedrock's per-minute token-throughput quota, whose standard error text is "Too many tokens, please wait before trying again." When the relay streams that error as content with a non-standard finish_reason, it surfaces to Pi as Provider finish_reason: error_finish and fails the turn immediately: Pi's built-in retry does not recognize the signature, and it must not be treated as context overflow.
How it works:
- When a turn ends in a recognized throttle error, ACP rewrites the error so Pi's native retry re-runs the same turn (no duplicate user message, the error is kept out of the LLM context, native TUI retry indicator). Pi's native budget is small and fast (3 attempts, 2s base).
- When a run still ends in a throttle error and ACP's budget allows, ACP waits a progressive delay (default: 60s, 120s, 240s, … capped at 5 minutes), then sends one auto-marked user message (starts with
[ACP:provider-throttle]) that resumes the interrupted step. - The model is instructed to resume where it left off (system-prompt note). Sending new input during a wait cancels the pending retry. When the budget is exhausted, the error is surfaced to you unchanged.
Not retried (deliberately left to Pi's own behavior or to fail-fast): real context-overflow errors (
prompt is too long, …), quota/billing exhaustion (quota exceeded,billing, …), and generic 429s.
Strict pacing (optional): By default ACP lets Pi's native fast retries run first and then paces on its own. If you want only ACP's paced kicks (e.g. a very tight tokens/minute quota), additionally set Pi's own retry off via
"retry": { "enabled": false }in~/.pi/settings.json.
throttleRetry
- Type:
boolean | object - Default:
true(object form with all defaults) - Status: 🟢 ACTIVE
- Description: Enable/disable auto-retry of provider token rate-limit errors and tune its budget.
throttleRetry: falsedisables the feature entirely. Object form (any subset):
{
"throttleRetry": {
"enabled": true,
"maxRetries": 10,
"baseDelayMs": 60000,
"maxDelayMs": 300000,
"backoffMode": "exponential"
}
}
throttleRetry.enabled
- Type:
boolean - Default:
true - Status: 🟢 ACTIVE
- Description: Turn the feature on/off.
false(or top-levelthrottleRetry: false) restores the original fail-fast behavior.
throttleRetry.maxRetries
- Type:
number(integer ≥ 1) - Default:
10 - Status: 🟢 ACTIVE
- Description: Total budget of ACP-driven retries for one error episode (a run ending in the same throttle error, plus the paced kicks it triggers). Any successful non-error response — or a new user message — starts a fresh episode. Exhausted → the error is surfaced to you as-is.
throttleRetry.baseDelayMs
- Type:
number(milliseconds) - Default:
60000 - Status: 🟢 ACTIVE
- Description: Delay before the first paced kick — sized for Bedrock's per-minute rolling quota window.
maxDelayMsis forced to at least this value.
throttleRetry.maxDelayMs
- Type:
number(milliseconds) - Default:
300000 - Status: 🟢 ACTIVE
- Description: Cap for paced kick delays in
"exponential"mode (60s → 120s → 240s → 300s → 300s … with defaults). Ignored in"fixed"mode, which always usesbaseDelayMs.
throttleRetry.backoffMode
- Type: string enum
"exponential" | "fixed" - Default:
"exponential" - Status: 🟢 ACTIVE
- Description: Delay progression between paced kicks:
"exponential"doubles the delay per kick (capped atmaxDelayMs);"fixed"repeatsbaseDelayMsevery kick.
Tool Call Repetition Guard
The repetitionGuard key breaks infinite loops of byte-identical tool calls. Small greedy-decoding models can get stuck re-emitting the exact same (tool call → tool result) pair turn after turn — e.g. calling acp_status {"scope":"uncompressed","view":"ranges"} dozens of times with identical arguments while context grows each round. Token-level penalties cannot break this, because it is a sequence-level attractor (the repetition crosses turn boundaries), not a within-sequence token repetition.
The guard fingerprints each tool call as sha1(toolName + canonical-JSON(args)), where the JSON serialization sorts object keys so that only the arguments matter — key order and result content are ignored. It tracks the length of the current run of consecutive identical calls per session:
- At
warnconsecutive identical calls, a strong warning is appended to the matching tool result telling the model to stop repeating the call. - At
abortconsecutive identical calls, the call is blocked (not executed), the turn is aborted, and a terminal notification is shown.
Any change to the arguments (or a switch to a different tool) resets the counter, as does a real user message (extension-sent messages do not reset it).
repetitionGuard
-
Type: boolean | object
-
Default:
true -
Status: 🟢 ACTIVE
-
Description: Enable/disable the repetition breaker and tune its thresholds.
repetitionGuard: falsedisables it entirely. Object form (any subset):{ "repetitionGuard": { "enabled": true, "warn": 3, "abort": 5 } }
repetitionGuard.enabled
- Type: boolean
- Default:
true - Status: 🟢 ACTIVE
- Description: Turn the feature on/off.
false(or top-levelrepetitionGuard: false) disables all repetition detection.
repetitionGuard.warn
- Type: number
- Default:
3 - Status: 🟢 ACTIVE
- Description: Number of consecutive byte-identical calls before a warning is appended to the tool result. Must be at least 1.
repetitionGuard.abort
- Type: number
- Default:
5 - Status: 🟢 ACTIVE
- Description: Number of consecutive byte-identical calls before the call is blocked and the turn is aborted. Must be greater than
warn; if misconfigured lower, it is clamped up towarn + 1.
Degeneration Guard
The degenerationGuard key handles character-level degeneration: a model occasionally gets stuck repeating one single codepoint — observed in the wild as a thinking block ending in 4655 consecutive 「【」, escalating over turns until the turn aborts and the session dies (#351). Unlike repetitionGuard (byte-identical tool-call loops), this is a token-level attractor inside generated text/thinking itself.
Why the adapter must act: pi replays prior assistant thinking back to the provider on every subsequent request (openai-completions sends it as reasoning_content, or as plain text when the model requires thinking-as-text), and an aborted turn's partial message persists in the session log. A degenerated tail therefore rides along on every later prompt, where the model sees its own previous output ending in thousands of repeated characters — a continuation bias that re-triggers the same degeneration, aborts the next turn too, and leaves the session with no recovery path.
On every context event the guard scans assistant text/thinking blocks in the outgoing view:
- Runs of one codepoint ≥
minRunare collapsed into a short marker (【【【… [4655× identical chars cut — degenerate repeat]) — up to 3 copies of the character are kept so the context stays legible. The pass is pure, idempotent and fail-safe; persisted history is never modified. - While the most recent assistant message is degenerated, a one-shot
[ACP recovery notice]user message is appended telling the model that the repeated segment carries no information and to resume from its last valid step. It is position-based self-limiting: once the model produces a fresh turn the old message is no longer last and the notice disappears — no persistent state, no accumulation. - A terminal notification echoes the collapse once per session + run signature.
Tool-call arguments are never rewritten (rewriting them would desync the model's view from the call that actually executed). Detection runs on the persisted originals, not the outgoing view: thinking-only aborted turns never reach the outgoing view (empty assistant text would 400 on OpenAI-compatible providers), yet they are still "the previous turn" for the model's continuation, so the notice fires there too.
degenerationGuard
-
Type: boolean | object
-
Default:
true -
Status: 🟢 ACTIVE
-
Description: Enable/disable the degenerate-repeat guard and tune its threshold.
degenerationGuard: falsedisables it entirely. Object form (any subset):{ "degenerationGuard": { "enabled": true, "minRun": 200 } }
degenerationGuard.enabled
- Type: boolean
- Default:
true - Status: 🟢 ACTIVE
- Description: Turn the feature on/off.
false(or top-leveldegenerationGuard: false) disables all degeneration detection and the recovery notice.
degenerationGuard.minRun
- Type: number
- Default:
200 - Status: 🟢 ACTIVE
- Description: Minimum length of a single-codepoint run (counted in codepoints, surrogate-pair safe) before it is treated as degeneration. Legitimate runs in coding sessions (markdown hrules, dotted leaders) stay well below this; observed pre-degeneration drift maxed at ~60 before the catastrophic 4655 run. Values below 8 are raised to 8 (keeps the collapse marker itself re-scan safe); invalid values fall back to 200 with a logged warning.
Compression Tuning
The compress sub-object groups the three thresholds that form a three-tier escalation for context management. They control when the model is nudged to compress and when large outputs are forcibly truncated to keep the session alive. Lower thresholds mean the extension compresses earlier and more aggressively.
The flow is:
- Growth-driven soft nudges (0–75%) — governed by
compress.nudgeGrowthTokens. - Forced nudges (75–95%) — once usage crosses
compress.maxContextLimit, nudges fire regardless of the growth gate. These are lossless. - Emergency truncation (95%+) — once usage crosses
compress.emergencyThresholdPercent, large tool outputs are truncated to prevent context overflow. This is lossy.
compress.maxContextLimit
- Type:
number | string - Default:
0.75(or"75%") - Status: 🟢 ACTIVE
- Description: The context-usage threshold that triggers forced compression nudges. Once usage reaches this level, nudges fire on every turn, bypassing the growth-gate and cadence checks that normally throttle them. Accepts a ratio (
0.75) or a percent string ("75%"). A lower value makes the extension compress earlier and more aggressively. Maps to the kernel settingnudge.maxContextLimitPct.
compress.emergencyThresholdPercent
- Type:
number | string - Default:
0.95(or"95%") - Status: 🟢 ACTIVE
- Description: The context-usage threshold that triggers emergency truncation of large tool outputs to keep the session alive when context is nearly full. Accepts a ratio (
0.95) or a percent string ("95%"). This value must be greater than or equal tocompress.maxContextLimit, otherwise the escalation order breaks. Maps to the kernel settingsnudge.emergencyThresholdPctandtruncate.threshold.
compress.nudgeGrowthTokens
- Type:
number - Default:
50000 - Status: 🟢 ACTIVE
- Description: The token-growth threshold that controls the cadence of soft compression nudges. A soft nudge fires roughly every time this many tokens of new compressible content accumulate. A lower value means the model is nudged to compress more often; a higher value means less frequent nudges. This only governs growth-driven nudges — once usage crosses
compress.maxContextLimit, forced nudges take over regardless of this setting. Maps to the kernel settingsnudge.growthFloorandnudge.growthCap. - Same-turn re-inject: within one user turn a nudge injects at most once, but once the context has since grown by a full growth floor (mirroring the kernel's anti-thrashing cadence:
max(minGrowthFloor, minGrowthRatio × adaptiveGrowth)— 22.5K tokens with defaults) a fresh reminder re-injects in the same turn (issue #269: a model that ignored a 78% nudge used to stay silent until the 95% emergency truncation). After a successful compress the growth baseline re-anchors to the new (smaller) scale, so post-compress regrowth into the pressure band is not held against the pre-compress peak.
compress.reasoning
-
Type:
object—{ "drop": boolean, "threshold": number } -
Default:
{ "drop": true, "threshold": 2048 } -
Status: 🟢 ACTIVE
-
Description: Config for dropping oversized reasoning (thinking) parts from historical
compresstool calls — exact semantic alignment with opencode-acp #377.compresscalls are hard-exempt from compression (their tool results anchor the block summaries), so their thinking rides along every request as an unreclaimable context floor. A request-time pass removesthinkingparts from a message only when all gates hold:- Closed turn — the message is strictly before the last genuine user message; the active round is never touched (some providers require replaying the active round's thinking).
- Selector — the message carries a
toolCallpart with namecompress(only compress; other protected tools would need their own explicit config). - Size — the message's total reasoning length (chars, summed across parts of that message, never across messages) strictly exceeds
threshold.0drops any non-empty reasoning.
Persisted history is never modified — the pass only rewrites the outgoing view, rebuilt fresh from the session log on every request. Pure, idempotent, fail-safe (any error leaves messages untouched). Merged field-wise (
drop,thresholdseparately) across the three levels ofcompress.providers.Fields:
drop(boolean, defaulttrue) — master switch;falsedisables the pass (kill-switch).threshold(number, chars, default2048) — single-thinking size gate.
Providers whose thinking items are opaque and must round-trip unmodified (e.g. OpenAI encrypted reasoning) can opt out per-provider:
{ "compress": { "providers": { "openai": { "reasoning": { "drop": false } } } } }Strict-echo thinking upstreams (auto-disabled). A few thinking-mode providers reject a rebuilt request with HTTP 400 (
The \reasoning_content` ... must be passed back to the API) once a closed-round assistant message loses its reasoning. The adapter detects **DeepSeek** statically — the model'sbaseUrlor provider name containsdeepseek(case-insensitive) — and forcesdrop: falsefor that model automatically, overriding an explicitdrop: truefor safety. This is cost-free for non-thinking DeepSeek models, which emit nothinkingparts to drop. Strict-echo providers **not** on adeepseek` host — GLM-thinking, QwQ, self-hosted DeepSeek mirrors — are deliberately not auto-detected (that would disable the pass for their non-thinking models); use the per-provider override above for those. Twin fixes: proxy-side billion-context#690 and kernel-side fold atomicity acp-kernel#245 (shipped in acp-kernel 0.0.63); tracked in #361.
compress.providers — per-provider & per-model overrides
- Type: object — a map of provider name →
{ ...<compress fields>, models: { modelId → <compress fields> } } - Default: (unset — global
compress.*applies to all models) - Status: 🟢 ACTIVE
- Description: Narrows the global thresholds for a specific Pi provider and/or a specific model, resolved live each turn from the active model. The three levels cascade per-field, deepest-wins:
model > provider > global. A field left undefined at a deeper level does not clear a shallower value — only a field you actually set overrides. Unknown providers/models fall back to the global thresholds.
The provider key is the Pi provider name (e.g. "anthropic", "openai", "zhipu") — the same name used in models.json and pi --provider. The model key is the model id (ctx.model.id). The adapter sits at Pi's model layer and never sees the upstream URL, so it matches providers by name, not by URL prefix like the billion-context proxy does.
{
"compress": {
"maxContextLimit": "75%",
"emergencyThresholdPercent": "95%",
"nudgeGrowthTokens": 50000,
"providers": {
"anthropic": {
"maxContextLimit": "80%",
"models": {
"claude-sonnet-4-5": { "maxContextLimit": "70%", "nudgeGrowthTokens": 30000 }
}
}
}
}
}
On anthropic / claude-sonnet-4-5 the effective thresholds become maxContextLimit=70%, nudgeGrowthTokens=30000, and emergencyThresholdPercent=95% (inherited from global).
Prompts Customization
The prompts object overrides acp-kernel's load-bearing compression prompt rules — the verbatim instructions the model receives about how to write summaries (keep full file paths, function signatures, decisions and rationale; drop verbose logs, etc.). These four fields are embedded into the system prompt and the compression nudge text:
| Field | What it governs |
|---|---|
compressPhilosophy | The two failure modes to avoid (over-/under-compression) and the single test for when to compress. |
howToCompressRules | Tier-1 rules: what to KEEP verbatim vs DROP, and the summary priority order. |
tier2DistillRules | Tier-2 distillation rules (decisions/outcomes only). |
tier3CondenseRules | Tier-3 ultra-condensation rules (bare facts). |
⚠️ Quality risk. These rules are tuned for retrieval quality. Replacing them with looser text can silently degrade summaries — lost paths, signatures, and decisions lead to worse reconstruction later. The
acknowledgePromptsRiskgate exists to make this an explicit, deliberate choice.
prompts
-
Type:
object(partial — omit fields to keep their defaults) -
Default: (kernel defaults) — the verbatim rules shipped with acp-kernel
-
Status: 🟢 ACTIVE
-
Description: Override one or more of the four compression prompt fields. Each field you set replaces the kernel default verbatim; fields you omit are inherited unchanged. Non-string values are silently dropped (only deliberate string overrides apply). Requires
acknowledgePromptsRisk: true— without it, every override is dropped and the defaults are used, with a warning logged. Example:{ "prompts": { "compressPhilosophy": "Compress aggressively; prefer signal over completeness.", "howToCompressRules": "Keep file paths + signatures verbatim. Drop verbose logs.", "tier2DistillRules": "Decisions and outcomes only; drop process and paths.", "tier3CondenseRules": "One line per block: bare facts only." }, "acknowledgePromptsRisk": true }
acknowledgePromptsRisk
- Type:
boolean - Default:
false - Status: 🟢 ACTIVE
- Description: The safety gate for
promptsoverrides. Set totrueto acknowledge that replacing the kernel's tuned compression rules may reduce summary quality, and to make yourpromptsoverrides take effect. Whenfalse(or omitted), allpromptsoverrides are ignored and the kernel defaults are used. IfresolvePromptsrejects your override (for example a malformed value that still passes the type check), the extension falls back to the defaults and logs aprompts-resolve-failedwarning rather than failing to start.
Environment Variables
Environment variables take precedence over the JSON config files. They are useful for one-off overrides, CI runs, and headless sessions where you want to avoid editing config files.
ACP_AUTO_UPDATE
- Type: string flag
- Default: (unset — auto-update follows the
autoUpdateconfig) - Status: 🟢 ACTIVE
- Description: Set to
0orfalseto disable auto-update (same effect as"autoUpdate": false). Leave unset to honor the config. This is the recommended way to disable startup network calls in locked-down environments without modifyingacp.json.
ACP_MODEL_CONTEXT_LIMIT
- Type: integer (tokens)
- Default: (unset — limit follows
modelContextLimit, then the live model context window) - Status: 🟢 ACTIVE
- Description: Override the context limit, in tokens. Takes the highest precedence — overrides the
modelContextLimitconfig value. Useful for forcing a specific limit in test harnesses and headless runs where model metadata is unavailable or unreliable.
ACP_DEBUG
- Type: string flag
- Default: (unset — debug logging follows the
debugconfig) - Status: 🟢 ACTIVE
- Description: Set to
1ortrueto enable debug-level logging. Equivalent to setting"debug": truein the config, but applied without editing a file. The always-on lifecycle/error/warning events are written regardless.
ACP_LOG_FILE
- Type: string (file path)
- Default:
~/.pi/acp.log - Status: 🟢 ACTIVE
- Description: Override the path to the log file. By default, structured logs are written to
~/.pi/acp.log(the file rotates to~/.pi/acp.log.oldat 10 MB). Point this at a different location to keep per-project or per-run logs separate.
PI_ACP_DELEGATE_MAX_DEPTH
- Type: integer ≥ 1
- Default: (unset — follows
delegate.maxDepth, then 2) - Status: 🟢 ACTIVE
- Description: Override the maximum delegate nesting depth for one session without editing config. Takes precedence over
delegate.maxDepth. The resolved value is what gets propagated down the delegation tree. Do not set this manually mid-tree: it is also the internal variable ACP uses to pass the effective limit into child processes.
PI_ACP_DELEGATE_SYNC_TIMEOUT_MINUTES
- Type: number (minutes) or
0 - Default: (unset — follows
delegate.syncTimeoutMinutes, then 5) - Status: 🟢 ACTIVE
- Description: Override the synchronous
acp_delegatehard timeout. Set0to disable the sync hard timeout for one session. Takes precedence overdelegate.syncTimeoutMinutes.
PI_ACP_DELEGATE_IDLE_TIMEOUT_MINUTES
- Type: number (minutes) or
0 - Default: (unset — follows
delegate.idleTimeoutMinutes, then 5) - Status: 🟢 ACTIVE
- Description: Override the async idle watchdog window. Set
0to disable the idle watchdog for one session (ACP logs a warning;acp_delegate_cancelremains as a manual escape hatch). Takes precedence overdelegate.idleTimeoutMinutes.
PI_ACP_DELEGATE_ASYNC_TIMEOUT_MINUTES
- Type: number (minutes) or
0 - Default: (unset — follows
delegate.asyncTimeoutMinutes, then 30) - Status: 🟢 ACTIVE
- Description: Override the absolute hard limit for async delegate children. Set
0to run long tasks without an absolute cap (the idle watchdog still applies unless disabled). Takes precedence overdelegate.asyncTimeoutMinutes.
PI_ACP_DELEGATE_MAX_CONCURRENT
- Type: integer (≥ 1)
- Default: (unset — cap follows
delegate.maxConcurrent, then unlimited) - Status: 🟢 ACTIVE
- Description: Override the background (
async) delegate concurrency cap. Takes precedence overdelegate.maxConcurrent. Set to1for forced serial execution. Invalid values fall back to the next source (then unlimited) with a warning rather than failing the session.