pi-openai-toolkit 0.14.12 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ # Toolkit configuration
2
+
3
+ Toolkit reads only `~/.pi/agent/extensions/pi-openai-toolkit/config.json`. There are no project overrides, environment policy overlays, profiles, `extends`, or wildcard rules. Pi still owns extension loading, provider endpoints, model registration, and credentials. A project-local extension installation does not create project-local Toolkit policy.
4
+
5
+ This reference describes schema v2. Unversioned legacy configuration remains supported. Configuration schema v2 and Remote Compaction v2 are separate version numbers.
6
+
7
+ ---
8
+
9
+ ### Start with a small document
10
+
11
+ Create the file and parent directory if absent. This example states the shipped feature defaults; it is not a conversion of an existing legacy file:
12
+
13
+ ```json
14
+ {
15
+ "schemaVersion": 2,
16
+ "defaults": {
17
+ "context": { "mode": "remote-compaction" },
18
+ "webSearch": { "route": "unmanaged" },
19
+ "imageGeneration": {
20
+ "enabled": false,
21
+ "defaultModel": "gpt-image-2.5",
22
+ "allowedModels": ["gpt-image-2.5"]
23
+ },
24
+ "autoMode": { "available": false }
25
+ },
26
+ "models": {},
27
+ "diagnostics": {
28
+ "level": "info",
29
+ "notifyOnLoad": false,
30
+ "captureRequests": false,
31
+ "captureResponses": false,
32
+ "redactSensitiveData": true
33
+ }
34
+ }
35
+ ```
36
+
37
+ `$schema` is optional editor metadata pointing to the shipped [config.schema.json](../config.schema.json). It does not change runtime schema selection. The editor schema describes canonical spelling; runtime also trims surrounding whitespace in supported strings and deduplicates lists. Runtime validation checks normalized key collisions and image-default membership that the editor schema cannot express.
38
+
39
+ ---
40
+
41
+ ### Resolution and errors
42
+
43
+ Precedence is built-ins, then `defaults`, then `models["provider/model-id"]`. Keys are exact and case-sensitive. Leading/trailing whitespace is trimmed; conflicting normalized keys are rejected. Model IDs may contain further slashes. No pattern matching or endpoint/capability guessing occurs.
44
+
45
+ Known nested objects merge by field. Missing fields inherit, arrays replace instead of append, and `false`/`0` retain their meanings. Only optional model references accept `null`: context producer, native-fallback model, auto reviewer, and classifier model. `null` elsewhere is invalid. Defaults do not prohibit exact overrides; an exact model can set `available: true` over a default of `false`.
46
+
47
+ A missing file uses independent built-in defaults. Invalid JSON, an unreadable file, missing/unsupported `schemaVersion` on a v2-shaped document, or mixed legacy/v2 fields are reported distinctly. Unknown v2 policy keys are errors in their owning scope. Invalid selected settings block dependent operations; they never silently pick a more permissive route. Errors in other models remain visible without replacing a valid current model's policy. A valid exact leaf may shadow an invalid default leaf, but cannot hide a malformed containing object or unknown field in that feature scope.
48
+
49
+ Each public callback, tool execution, compaction, reviewer/classifier operation, or config command uses an immutable snapshot across its awaited helpers. The next operation reads again. This does not make separate Pi headers, payload, and tool events one atomic transaction. Engagement, overrides, scores, windows, and results stay out of the config file.
50
+
51
+ ---
52
+
53
+ ### Context
54
+
55
+ Place `context` under `defaults` or an exact `models` entry.
56
+
57
+ | Relative field | Default | Contract |
58
+ | --- | --- | --- |
59
+ | `mode` | `"remote-compaction"` | `"pi"` relinquishes Toolkit context management; `"remote-compaction"` uses the eligible Responses checkpoint path; `"remote-windows"` selects Codex windows where supported. |
60
+ | `remoteCompaction.model` | `null` | Exact producer reference; `null` uses the active model. It must share the effective base URL required by the existing compaction path. |
61
+ | `remoteCompaction.inputSource` | `"legacy"` | `"legacy"` preserves session/raw-branch input; `"pi-context-hook"` opts into Pi's ordered context projection. Checkpoint provenance must match. |
62
+ | `remoteCompaction.allowContinuityBreak` | `false` | Allow restarting from Pi context after a foreign compaction entry. It does not make malformed opaque checkpoints replayable. |
63
+ | `remoteCompaction.apis` | `["openai-responses", "openai-codex-responses"]` | May narrow this set; `[]` permits no remote-compaction API. Unsupported entries are errors. |
64
+ | `nativeFallback.enabled` | `true` | Enable the existing native-method fallback tier. |
65
+ | `nativeFallback.model` | `null` | Optional summary-model reference on the remote-ineligible path. After a remote attempt fails, the existing producer-first fallback order is retained. |
66
+ | `nativeFallback.thinkingLevel` | `"off"` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; actual model support still applies. |
67
+ | `remoteWindows.leaveManagedMode` | `"warn"` | `"compact"` also requests a close-out when retired history exceeds 80% of the next model's window and compaction can finish. Print mode remains warning-only; turn-end close-out requires a queued trim. |
68
+ | `remoteWindows.reminderThresholdPercent` | `5` | Integer 0-100. `0` disables both the once-per-window reminder and exhausted-window fallback. |
69
+
70
+ Native Codex eligibility derives from the actual Pi provider/API. Gateways additionally need the exact transport opt-in below. `remote-windows` retains the compaction chain for models outside window capability. A configured window model whose activation fails does not silently receive a summary fallback. The rollover notes gate, durable history, producer/consumer identities, and checkpoint provenance are unchanged. See [Toolkit internals](internals.md).
71
+
72
+ ---
73
+
74
+ ### Compatibility transport
75
+
76
+ Only `models["provider/model-id"].compatibility.transport` is configurable: `"standard"` is the internal baseline and `"codex-gateway"` is an exact opt-in. There is no global `defaults.compatibility` and no built-in gateway model list.
77
+
78
+ This selects an existing Toolkit protocol profile, independently of context mode. It does not change Pi's provider, API, endpoint, or model. The same decoded document supplies compatibility for search, image generation, and a separately named compaction producer. A successful config resolution is not a backend capability test.
79
+
80
+ ---
81
+
82
+ ### Web Search
83
+
84
+ `defaults.webSearch.route` and `models[exact].webSearch.route` accept:
85
+
86
+ | Route | Behavior |
87
+ | --- | --- |
88
+ | `unmanaged` | Default. Release Toolkit ownership and remove Toolkit-owned standalone exposure. Third-party search and unrelated network tools remain possible. |
89
+ | `local` | Restore the original local tool state and remove native/standalone search conflicts from provider payloads. Never activate a previously inactive local tool. |
90
+ | `hosted` | Use native Responses search with source annotations; suppress conflicting local and standalone tools. |
91
+ | `standalone-alpha` | Experimental, explicit opt-in. Expose sequential `web_run` and call the provider-relative `/alpha/search` endpoint once per execution. |
92
+
93
+ There is no v2 search master switch. `local` and `unmanaged` differ in payload cleanup and ownership. Missing auth, an unavailable route, a tool conflict, or invalid selected policy does not trigger another route. Standalone supports `search_query`, `image_query`, `open`, `click`, `find`, `screenshot`, `finance`, `weather`, `sports`, and `time`, with `response_length`. It sends a bounded command envelope rather than the full transcript; follow-up references depend on the provider's session/reference handling. The gateway must expose the endpoint and its standalone-search capability.
94
+
95
+ ---
96
+
97
+ ### Images
98
+
99
+ Image settings are global-only under `defaults.imageGeneration`. `enabled` defaults to `false`. `defaultModel` defaults to `"gpt-image-2.5"`; `allowedModels` defaults to `["gpt-image-2.5"]`.
100
+
101
+ These are bare output-model IDs, not active-session model keys. Each normalized ID must contain 1-256 characters. The allowed list must remain nonempty after normalization and contain the explicit default. Order has no default-selection meaning. A caller's one-call `model` must be a member; policy and membership are checked before auth, local-reference preparation/upload, and paid dispatch. The active session remains the Responses routing model. Config does not grant permission to upload arbitrary local files or imply that a provider supports a particular image model.
102
+
103
+ ---
104
+
105
+ ### Auto Mode
106
+
107
+ Place `autoMode` under `defaults` or an exact model. `available` permits engagement; it does not engage the runtime. Use `/auto on` or `--auto`, and `/auto off` to disengage. If an engaged session encounters invalid policy, calls remain blocked until policy is corrected or the user explicitly turns the gate off. A review timeout never means approval.
108
+
109
+ | Relative field | Default | Contract |
110
+ | --- | --- | --- |
111
+ | `available` | `false` | Model eligibility for engagement. |
112
+ | `reviewerModel` | `null` | Exact review-model reference; required for useful engagement. |
113
+ | `gate` | `"side-effect"` | Reviews `bash`, `write`, `edit`, and `extraTools`; `"all"` reviews every tool. Session gate overrides remain runtime state. |
114
+ | `extraTools` | `[]` | Additional names for the side-effect gate. |
115
+ | `timeoutMs` | `30000` | Integer 1000-120000 milliseconds. |
116
+ | `transcript` | `true` | Include transcript context in the blocking review. |
117
+ | `evidenceTools` | `true` | Permit the reviewer's existing read-only evidence tools. |
118
+ | `maxEvidenceRounds` | `3` | Integer 0-8. |
119
+ | `classifier.enabled` | `false` | Enable nonblocking trajectory pre-scoring. |
120
+ | `classifier.model` | `null` | Exact reference; `null` uses the reviewer. |
121
+ | `classifier.timeoutMs` | `15000` | Integer 1000-120000 milliseconds. |
122
+ | `classifier.maxLag` | `2` | Integer 0-20. |
123
+ | `circuitBreaker.consecutiveDenials` | `3` | Integer 0-100; `0` disables this limit. |
124
+ | `circuitBreaker.recentDenials` | `10` | Integer 0-100; `0` disables this limit. |
125
+ | `circuitBreaker.windowSize` | `50` | Integer 1-200, recent-verdict window size. |
126
+
127
+ Reviewer and classifier references are not active-session overrides. Classifier scores, denial history, and human decisions never become config entries.
128
+
129
+ ---
130
+
131
+ ### Diagnostics and inspection
132
+
133
+ Diagnostics are plugin-wide, not model-scoped.
134
+
135
+ | Field | Default | Contract |
136
+ | --- | --- | --- |
137
+ | `level` | `"info"` | `error`, `warn`, `info`, or `debug`. Debug enables lifecycle/compaction debug artifacts. Configuration issues and operation-blocking failures remain visible at every level. |
138
+ | `notifyOnLoad` | `false` | Optional informational load notification. |
139
+ | `captureRequests` | `false` | Independently opt into provider request artifacts. |
140
+ | `captureResponses` | `false` | Independently capture compact responses; this is not an ordinary chat-response logger. |
141
+ | `redactSensitiveData` | `true` | Optional additional redaction. Critical credentials, account IDs, and opaque encrypted data are always redacted. |
142
+ | `artifactRoot` | `"~/.pi/agent/artifacts/pi-openai-toolkit/compaction"` | Relative paths resolve against the canonical config directory, not the current project. |
143
+
144
+ `/toolkit-config` (or `show`) displays effective values with built-in/default/exact/legacy origins. `/toolkit-config validate` reports document issues, including unrelated model entries. Reports are bounded and redacted; unknown field names are masked. Repeated issue notifications are deduplicated within a session and do not depend on compaction enablement or debug mode.
145
+
146
+ `/toolkit-config migration-preview` analyzes a legacy file without changing it. These are human commands using Pi's UI; no model tool, new headless stdout protocol, network probe, auth lookup, tool mutation, or persistent state write is added. Headless operation failures and typed resolution issues remain observable. A truncated candidate is not usable JSON.
147
+
148
+ ---
149
+
150
+ ### Legacy compatibility and migration
151
+
152
+ Do not combine unversioned roots (`compaction`, `webSearch`, `imageGeneration`, `autoMode`) with `schemaVersion: 2`. Valid legacy behavior is retained through a compatibility adapter, including nullable model clears, legacy image-list defaults, and source-dependent hosted-search failure handling. Invalid selected legacy route values are now blocked instead of disappearing into another selection.
153
+
154
+ The preview maps recognized fields into a candidate, supplies leaf origins, lists unmapped/dormant paths, and reports `ready`, `needs-review`, `already-v2`, or `unavailable`. `ready` means no known semantic difference was found in recognized active policy; it is not authorization to apply or proof of provider support. Unknown names are masked and their values are never copied into the report. The original source bytes remain the authoritative copy of unknown and dormant content.
155
+
156
+ | Legacy input | v2 destination |
157
+ | --- | --- |
158
+ | `compaction.enabled` / `contextManagement` | `defaults.context.mode` |
159
+ | `compaction.remoteCompactModel`, `remoteV2ContextSource`, `allowCompactionContinuityBreak`, `responsesApis` | `defaults.context.remoteCompaction` fields |
160
+ | `compaction.nativeFallback` | `defaults.context.nativeFallback` |
161
+ | `compaction.leaveManagedMode`, `contextReminderThresholdPercent` | `defaults.context.remoteWindows` fields |
162
+ | `compaction.gatewayContextModels` | Exact `models[key].compatibility.transport` |
163
+ | `webSearch.defaultRoute`, `routes`, `models` | Default/exact search route candidates, subject to review below |
164
+ | `imageGeneration.models` | `allowedModels` plus an explicit default taken from the legacy first entry |
165
+ | `autoMode.enabled` / `models` | Default/exact `autoMode.available` |
166
+ | Legacy auto reviewer/classifier/breaker controls | Corresponding `autoMode` controls |
167
+ | Legacy compaction debug/capture/path settings | `diagnostics` |
168
+
169
+ The preview cannot claim universal lossless conversion:
170
+
171
+ - A legacy hosted allowlist ignores unsupported APIs and malformed payloads; explicit v2 hosted selection fails closed. Moving a list entry to an exact hosted route changes those failure paths.
172
+ - Disabled legacy features can retain dormant model lists/routes. A simplified v2 policy cannot preserve enable-later intent without review.
173
+ - Gateway compatibility is now shared independently of context mode; review effects on compaction and image affinity.
174
+ - Unknown fields, invalid normalization, or candidate validation failures require review. `remoteV2ContextSource: "legacy"` remains `inputSource: "legacy"`; preview does not upgrade checkpoint provenance or opt any model into standalone search.
175
+
176
+ There is no automatic migration, writer, or `--write` option. Before a separately approved manual cutover, back up the original bytes and verify that every installed Toolkit copy supports v2. Older copies, including 0.14.13, do not support this format. Updating source in a checkout does not update an installed package. Keep the legacy file until that cutover is ready.
package/docs/internals.md CHANGED
@@ -6,6 +6,7 @@ Developer-facing reference for [pi-openai-toolkit](../README.md). The README kee
6
6
 
7
7
  ### Table of Contents
8
8
 
9
+ - [Configuration resolution](#configuration-resolution)
9
10
  - [Remote Context protocol](#remote-context-protocol)
10
11
  - [Rollover lifecycle](#rollover-lifecycle)
11
12
  - [Remote Compaction v2 wire contract](#remote-compaction-v2-wire-contract)
@@ -15,6 +16,16 @@ Developer-facing reference for [pi-openai-toolkit](../README.md). The README kee
15
16
 
16
17
  ---
17
18
 
19
+ ### Configuration resolution
20
+
21
+ The single global document is loaded through [src/config.ts](../src/config.ts). [src/config/legacy.ts](../src/config/legacy.ts) owns unversioned compatibility; [src/config/v2.ts](../src/config/v2.ts) validates v2 scopes, resolves exact policy, and records leaf origins. [src/config/policy.ts](../src/config/policy.ts) defines effective values independently of persisted session data. See the [configuration reference](configuration.md) for supported fields and migration limits.
22
+
23
+ Feature entrypoints resolve once per public operation and pass that immutable snapshot through awaited helpers. A separately named compaction producer is resolved from the same decoded document for its own identity. Subsequent callbacks reload; there is no cross-event provider transaction, project lookup, singleton cache, or config state writer. Internal legacy-shaped engine adapters preserve compaction/search algorithms without exposing v2 parsing to consumers. Cross-feature gateway policy belongs to the resolver, not the context enable switch.
24
+
25
+ Selected invalid policy blocks dependent operations; unrelated model errors remain reportable without replacing the selected model. An engaged auto gate stays blocking when its policy becomes invalid. Image choices are checked before auth, uploads, or paid requests. The human `/toolkit-config` command inspects effective values, validation, or a migration candidate without action APIs or network access. Migration is preview-only; source bytes and unknown/dormant content stay untouched.
26
+
27
+ ---
28
+
18
29
  ### Remote Context protocol
19
30
 
20
31
  Internal alpha endpoints, adapted from `@howaboua/pi-codex-conversion`:
@@ -29,7 +40,7 @@ The native route uses `{base} = <backend-api>/codex` with `Authorization: Bearer
29
40
 
30
41
  Search text and note bodies are sent under the encrypted-argument policy. Live model requests carry `x-codex-window-id` and `x-codex-turn-metadata` request headers derived from the active window identity.
31
42
 
32
- Coverage is never inferred from model names. A native session uses provider `openai-codex` with API `openai-codex-responses` for any model ID. A gateway session uses API `openai-responses` and must carry an exact `provider/model` key listed in `compaction.gatewayContextModels`; the list ships empty and there is no built-in gateway SKU. See [src/context-management/codex-provider.ts](../src/context-management/codex-provider.ts). The toolkit registers no provider or model with Pi. It resolves credentials and base URLs through Pi's `ModelRegistry`.
43
+ Coverage is never inferred from model names. A native session uses provider `openai-codex` with API `openai-codex-responses` for any model ID. A gateway session uses API `openai-responses` and must carry an exact `provider/model` key whose `models[exact].compatibility.transport` is `"codex-gateway"` (legacy `compaction.gatewayContextModels`); no gateway is opted in by default. See [src/context-management/codex-provider.ts](../src/context-management/codex-provider.ts). The toolkit registers no provider or model with Pi. It resolves credentials and base URLs through Pi's `ModelRegistry`.
33
44
 
34
45
  ---
35
46
 
@@ -63,26 +74,32 @@ State invariants the lifecycle code must keep honest:
63
74
  - The checkpoint gate verifies a successful `notes` `append_to_file` or `write_file` against the persisted session branch after the latest window boundary, so it survives restarts and forks. A response with `ok: false` or `success: false` is not successful, even when HTTP returns 200.
64
75
  - The gate uses the live session/branch identity from `ctx.sessionManager`; cached manager identity must not hide a valid current-window pair, and old or foreign-session evidence must remain rejected.
65
76
  - Every rollover carries the most recent successful checkpoint path in its handoff message. The optional `thread_hint` is supplemental; a hint failure must not erase the local receipt.
66
- - A successful rollover closes the pre-rollover checkpoint phase. The first turn in the new window reads the receipt once and resumes the active user task; it must not immediately create another checkpoint or call `new_context` as part of that handoff. A later checkpoint is only for a later rollover.
77
+ - A successful rollover closes the pre-rollover checkpoint phase. The first turn in the new window reads the receipt once and resumes the active user task; it must not immediately create another checkpoint or call `new_context` as part of that handoff. A later checkpoint is only for a later rollover, and `canRolloverFromCurrentWindow()` enforces it: a window entered by a rollover has to produce a tool result from a tool that is not one of `new_context`, `get_context_remaining`, `history`, or `notes` before it can roll over again.
78
+ - Pi 0.86 sends the system prompt and the entire tool loadout as `system` messages inside the transcript: `Agent#createContextSnapshot()` passes messages only, and the Responses transport rebuilds `params.tools` from the leading system message. Trimming everything before a window boundary therefore deletes the model's only tool declaration, and Pi never re-declares it because the persisted transcript still looks unchanged. `preserveSystemHead()` re-anchors the merged pre-boundary state as the leading message of every trimmed window.
79
+ - When even that rebuild would lose declared tools, the trim is refused and the full message list is sent: a window that still carries the previous turns is usable, a window with no tools silently degrades into a chat the model can only narrate. Each refusal is reported once per window and lost-tool set through `takeProjectionDiagnostics()`, which writes a `context.projection.tool_loadout_shrink` compaction artifact and warns in the UI.
80
+ - The manager also remembers the head it last anchored. When a boundary compaction retires the transcript entry that declared the tools, the branch itself no longer contains a system message, and the remembered head is re-anchored instead of sending a tool-less window. The cache is request-time only, refreshed by every projection, and cleared by `reset()`.
67
81
  - `new_context` has no force escape hatch: a persisted successful notes checkpoint in the current window is always required before rollover. The model must wait for the notes result to be persisted before retrying.
68
82
  - After a marker is accepted for sending, the manager keeps a session/window-anchored pending guard until that exact target marker is persisted or the session changes. A duplicate `new_context` during this persistence gap returns `started: false` and sends no second marker; projected/in-memory messages do not retire the guard.
69
83
  - Budget checks are skipped until the current window has produced its own assistant usage. Acting on the previous window's usage anchor would burn the once-per-window reminder on a false alarm.
70
84
  - The scheduled trim is consumed exactly once, synchronously, by the first compaction attempt whose boundary window ID matches. Every other compaction path, including a threshold without a scheduled rollover, manual `/compact`, and overflow, is cancelled.
71
85
  - A window boundary is persisted as a `codex-context-window` custom message. `session_start` replays boundaries from the branch and rebuilds identity after forks.
86
+ - Retiring windows is a projection-level act: the session branch keeps every retired window until a compaction consumes the scheduled trim. `evaluateWindowBulk()` measures that gap and compares it with the model about to receive it, `decideBulkCliffAction()` maps the report onto the `leaveManagedMode` policy (`warn` default, `compact`), and `decideBulkCloseOut()` only allows the automatic compaction where a compaction can actually finish: never in print mode (the process exits with the turn and would kill the summary), and at turn end only while a rollover trim is queued, because the window policy cancels every other compaction. Each window and model is surfaced once, and every decision writes a `window-bulk.<action>` compaction artifact with its trigger.
87
+ - The close-out runs from `model_select` and `agent_settled`, never from `before_agent_start`: `ExtensionContext.compact()` aborts the running turn, and a hook inside turn processing is exactly the busy lane it must not fight.
88
+ - A configured native-fallback summary model is checked against the size of the request Pi is about to send it (`estimateSummarizationRequest`). A narrower summary model than the active one - 272k summarizing a 400k session - returns `model-window-too-small` before authentication or the model call, so the current model gets the first attempt instead of a terminated stream.
72
89
 
73
90
  ---
74
91
 
75
92
  ### Remote Compaction v2 wire contract
76
93
 
77
- v2 is what an uncovered session gets. Any model that Remote Context declines, such as a gateway model absent from `gatewayContextModels`, can still compact through v2 when its API appears in `responsesApis`. The two strategies never compete for one session.
94
+ v2 is what an uncovered session gets. Any model that Remote Context declines, such as a gateway without an exact transport opt-in, can still compact through v2 when its API appears in `context.remoteCompaction.apis`. The two strategies never compete for one session.
78
95
 
79
96
  A `compaction_trigger` item appended to the live streaming request yields one output item of `type: "compaction"` with non-empty `encrypted_content`, stored in `CompactionEntry.details.compactedWindow`. On later requests the opaque checkpoint is replayed ahead of live turns, with no text summary. Replay fails closed: if the summary anchor cannot be located, the request is aborted with a notification and a content-free failure artifact. The sentinel-only payload is never sent.
80
97
 
81
98
  A v2 response with a missing or empty checkpoint is never stored, and the `nativeFallback` tier is skipped. Pi's own threshold drives the next attempt, which may use `remoteCompactModel` when configured. `remoteCompactModel` must resolve to the same effective base URL as the active model.
82
99
 
83
- Pi 0.85.1's `session_before_compact` event supplies preparation and branch data, while the direct Remote V2 client can bypass the provider `context` hook chain. To preserve compatibility, an omitted `compaction.remoteV2ContextSource` keeps the original `"legacy"` behavior: first compaction uses `buildSessionContext()` and falls back to `SessionBeforeCompactEvent.preparation`, while recursion uses the opaque window plus the raw branch tail.
100
+ Pi 0.85.1's `session_before_compact` event supplies preparation and branch data, while the direct Remote V2 client can bypass the provider `context` hook chain. To preserve compatibility, an omitted `context.remoteCompaction.inputSource` keeps the original `"legacy"` behavior: first compaction uses `buildSessionContext()` and falls back to `SessionBeforeCompactEvent.preparation`, while recursion uses the opaque window plus the raw branch tail.
84
101
 
85
- Setting `compaction.remoteV2ContextSource: "pi-context-hook"` opts into a narrow runtime bridge around Pi's public `ExtensionRunner.createContext()`. It adds a non-enumerable `ctx.projectContextForCompaction(messages)` method backed by `ExtensionRunner.emitContext()`. In this opt-in mode, Remote V2 first reads `buildSessionContext()` and then runs the ordered projection. If the bridge, session context, or recursive summary anchor is unavailable, the extension cancels rather than sending an unprojected history. The legacy mode is an intentional compatibility trade-off and does not claim parity with provider-visible context hooks.
102
+ Setting `context.remoteCompaction.inputSource: "pi-context-hook"` opts into a narrow runtime bridge around Pi's public `ExtensionRunner.createContext()`. It adds a non-enumerable `ctx.projectContextForCompaction(messages)` method backed by `ExtensionRunner.emitContext()`. In this opt-in mode, Remote V2 first reads `buildSessionContext()` and then runs the ordered projection. If the bridge, session context, or recursive summary anchor is unavailable, the extension cancels rather than sending an unprojected history. The legacy mode is an intentional compatibility trade-off and does not claim parity with provider-visible context hooks.
86
103
 
87
104
  New checkpoints record `inputProvenance: "pi-context-hook-v1"` or `"legacy-raw-context-v1"`, and replay/recursion reject missing or mode-mismatched markers without searching past the latest compaction. Retained `role: "custom"` messages are optional during replay because they may be changed or removed by context hooks; required user, assistant, and complete tool-call/result content remains ordered and fail-closed.
88
105
 
@@ -107,11 +124,11 @@ This is a version-coupled adapter, not a stable Pi extension contract. It is fea
107
124
 
108
125
  ### Artifacts and debugging
109
126
 
110
- With `debug: true`, the toolkit writes lifecycle and compaction artifacts under `artifactRoot` (default `~/.pi/agent/artifacts/pi-openai-toolkit/compaction`):
127
+ With `diagnostics.level: "debug"`, the toolkit writes lifecycle and compaction artifacts under `diagnostics.artifactRoot` (default `~/.pi/agent/artifacts/pi-openai-toolkit/compaction`):
111
128
 
112
129
  - Each `session_start` writes a lifecycle artifact whose `activation` field records `active` or the exact inactive reason, such as `unsupported-model`, `unsupported-api`, `missing-api-key`, `auth-resolution-failed`, or `tool-name-conflict`.
113
- - `logProviderPayloads` additionally writes provider request metadata and payloads, while `logCompactResponses` writes compact status and structural summaries with complete response and body fields critically redacted. Keep both off unless inspecting a specific request.
114
- - Artifacts always redact Authorization credentials, API keys and tokens, Codex account IDs, and opaque `encrypted_content` or `encrypted_output`, even when `redactSensitiveData` is `false`.
130
+ - `diagnostics.captureRequests` independently writes provider request metadata and payloads, while `diagnostics.captureResponses` writes compact status and structural summaries with complete response and body fields critically redacted. Keep both off unless inspecting a specific request.
131
+ - Artifacts always redact Authorization credentials, API keys and tokens, Codex account IDs, and opaque `encrypted_content` or `encrypted_output`, even when `diagnostics.redactSensitiveData` is `false`.
115
132
 
116
133
  ---
117
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-toolkit",
3
- "version": "0.14.12",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -46,6 +46,13 @@
46
46
  "src/compaction-projection.ts",
47
47
  "src/pi-context-hook.ts",
48
48
  "src/config.ts",
49
+ "src/config-command.ts",
50
+ "src/config/policy.ts",
51
+ "src/config/legacy.ts",
52
+ "src/config/v2.ts",
53
+ "src/config/migration.ts",
54
+ "src/config/notifications.ts",
55
+ "config.schema.json",
49
56
  "src/debug.ts",
50
57
  "src/details-store.ts",
51
58
  "src/deferred-tool-carryover.ts",
@@ -54,6 +61,7 @@
54
61
  "src/context-management/messages.ts",
55
62
  "src/context-management/window-budget.ts",
56
63
  "src/context-management/window-request.ts",
64
+ "src/context-management/window-bulk.ts",
57
65
  "src/context-management/window-manager.ts",
58
66
  "src/context-management/codex-provider.ts",
59
67
  "src/context-management/history-notes.ts",
@@ -96,6 +104,7 @@
96
104
  "src/web-search/prompt.ts",
97
105
  "src/web-search/types.ts",
98
106
  "docs/internals.md",
107
+ "docs/configuration.md",
99
108
  "README.md",
100
109
  "README.zh.md",
101
110
  "LICENSE",
@@ -1,6 +1,7 @@
1
1
  import type { AgentTool } from "@earendil-works/pi-agent-core";
2
2
  import { createReadOnlyTools, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { loadToolkitConfig } from "../config";
3
+ import { loadToolkitConfig, resolveToolkitConfig, type ResolvedToolkitConfig } from "../config";
4
+ import { notifyConfigIssues } from "../config/notifications";
4
5
  import {
5
6
  DEFAULT_AUTO_MODE_CONFIG,
6
7
  type AutoModeConfig,
@@ -152,6 +153,12 @@ export function registerAutoModeExtension(
152
153
  let callIndex = 0;
153
154
  let scoringInFlight = false;
154
155
 
156
+ function readConfig(ctx: ExtensionContext, model = ctx.model): ResolvedToolkitConfig {
157
+ const resolved = resolveToolkitConfig(loadConfig(), model);
158
+ notifyConfigIssues(ctx, resolved);
159
+ return resolved;
160
+ }
161
+
155
162
  function updateStatus(ctx: ExtensionContext, reviewingTool?: string): void {
156
163
  if (!ctx.hasUI) return;
157
164
  if (ctx.mode === "tui") {
@@ -188,7 +195,7 @@ export function registerAutoModeExtension(
188
195
  if (!isAutoModeEligible(ctx.model, config)) {
189
196
  if (ctx.hasUI) {
190
197
  ctx.ui.notify(
191
- "Auto mode needs an allowlisted model in autoMode.models and a configured autoMode.reviewerModel.",
198
+ "Auto mode needs availability for the current model and a configured reviewerModel.",
192
199
  "warning",
193
200
  );
194
201
  }
@@ -373,10 +380,14 @@ export function registerAutoModeExtension(
373
380
  pi.registerCommand(AUTO_MODE_COMMAND, {
374
381
  description: "Model-reviewed tool approval: /auto [on|off|status|all|side]",
375
382
  handler: async (args, ctx) => {
376
- const { config } = loadConfig();
377
- const auto = config.autoMode;
378
- applyConfiguredGate(runtime, auto);
383
+ const resolved = readConfig(ctx);
384
+ const auto = resolved.config.autoMode;
379
385
  const sub = args.trim().toLowerCase();
386
+ if (resolved.invalidFeatures.includes("autoMode") && sub !== "off") {
387
+ if (ctx.hasUI) ctx.ui.notify("Auto-mode configuration is invalid; the existing gate remains engaged until /auto off or a valid configuration is supplied.", "error");
388
+ return;
389
+ }
390
+ applyConfiguredGate(runtime, auto);
380
391
 
381
392
  switch (sub) {
382
393
  case "on":
@@ -419,7 +430,8 @@ export function registerAutoModeExtension(
419
430
  });
420
431
 
421
432
  pi.on("session_start", (event, ctx) => {
422
- const { config } = loadConfig();
433
+ const resolved = readConfig(ctx);
434
+ const { config } = resolved;
423
435
  toolReviewRenderer.clear();
424
436
  if (
425
437
  !toolReviewRenderer.supported &&
@@ -438,7 +450,8 @@ export function registerAutoModeExtension(
438
450
  resetScoreTracker(tracker);
439
451
  resetRejectionBreaker(breaker);
440
452
  callIndex = 0;
441
- const startedFromFlag = pi.getFlag(AUTO_MODE_FLAG) === true && engage(ctx, config.autoMode);
453
+ const requestedFromFlag = pi.getFlag(AUTO_MODE_FLAG) === true;
454
+ const startedFromFlag = requestedFromFlag && (resolved.invalidFeatures.includes("autoMode") ? (runtime.engaged = true) : engage(ctx, config.autoMode));
442
455
  if (startedFromFlag && event.reason === "startup" && ctx.mode === "tui") {
443
456
  ctx.ui.notify(`Auto mode on. Reviewing: ${describeGate(runtime)}`, "info");
444
457
  }
@@ -446,8 +459,9 @@ export function registerAutoModeExtension(
446
459
  });
447
460
 
448
461
  pi.on("model_select", (event, ctx) => {
449
- const { config } = loadConfig();
450
- if (runtime.engaged && !isAutoModeEligible(event.model, config.autoMode)) {
462
+ const resolved = readConfig(ctx, event.model);
463
+ const { config } = resolved;
464
+ if (!resolved.invalidFeatures.includes("autoMode") && runtime.engaged && !isAutoModeEligible(event.model, config.autoMode)) {
451
465
  disengage(ctx);
452
466
  if (ctx.hasUI) ctx.ui.notify("Auto mode off: this model is not allowlisted.", "warning");
453
467
  }
@@ -455,7 +469,9 @@ export function registerAutoModeExtension(
455
469
  });
456
470
 
457
471
  pi.on("before_agent_start", (_event, ctx) => {
458
- const { config } = loadConfig();
472
+ const resolved = readConfig(ctx);
473
+ const { config } = resolved;
474
+ if (resolved.invalidFeatures.includes("autoMode")) { resetScoreTracker(tracker); updateStatus(ctx); return; }
459
475
  applyConfiguredGate(runtime, config.autoMode);
460
476
  if (runtime.engaged && !isAutoModeEligible(ctx.model, config.autoMode)) {
461
477
  disengage(ctx);
@@ -472,8 +488,12 @@ export function registerAutoModeExtension(
472
488
  callIndex += 1;
473
489
  if (!runtime.engaged) return undefined;
474
490
 
475
- const { config } = loadConfig();
476
- const auto = config.autoMode;
491
+ const resolved = readConfig(ctx);
492
+ if (resolved.invalidFeatures.includes("autoMode")) {
493
+ resetScoreTracker(tracker);
494
+ return { block: true, reason: "Auto-mode configuration is invalid. The gate remains engaged; fix the configuration or explicitly use /auto off." };
495
+ }
496
+ const auto = resolved.config.autoMode;
477
497
  applyConfiguredGate(runtime, auto);
478
498
 
479
499
  if (!isAutoModeEligible(ctx.model, auto)) {
@@ -622,8 +642,10 @@ export function registerAutoModeExtension(
622
642
 
623
643
  pi.on("tool_result", (event, ctx) => {
624
644
  if (!runtime.engaged) return undefined;
625
- const { config } = loadConfig();
645
+ const resolved = readConfig(ctx);
646
+ const { config } = resolved;
626
647
  const auto = config.autoMode;
648
+ if (resolved.invalidFeatures.includes("autoMode") || !isAutoModeEligible(ctx.model, auto)) { resetScoreTracker(tracker); return undefined; }
627
649
  if (auto.classifier.enabled && shouldReviewTool(event.toolName, runtime)) {
628
650
  scheduleClassification(ctx, auto, {
629
651
  toolName: event.toolName,