NAVIGATION
ELOWEN DOCUMENTATION

Last updated: 10 October 2026

Browse documentation · Tool deferral and code mode
Developer reference

Tool deferral and code mode

Tool deferral and the core set

One policy decides which tools every route keeps fully declared: resolveToolDeferralDecisions in src/brain/toolSearch/deferralPolicy.ts. Its precedence is the global switch, the locked core set (CORE_TOOLS: Bash, Read, Edit, Grep, Write, picked from this instance's call counts), a per-tool choice, a per-source choice, the owning plugin's deferLoading, and finally the automatic threshold: when more than toolDeferThreshold tools (default 10) have no decision yet, all of them are deferred, whatever their source. No family is exempt from deferral: read-only tools follow the same rules as any other tool, and the Lsp tools come from the lsp plugin. Plan-mode admission is separate and set by CORE_TOOL_DEFAULTS in src/brain/toolMetadata.ts, which lists Delegate, exec and wait among others. ToolSearch and model-only interaction tools (AskUserQuestion, ExitPlanMode, ShowView, classified in composeSessionTools in src/brain/session/capabilities.ts) are not candidates: a session resolves the policy over loadable tools and adds the loader only when something is deferred. It is plan-safe, so a planning turn can load deferred read-only tools.

composeSessionTools resolves the decision once per session composition, and every route derives from that one set:

  • Local ToolSearch. Deferred tools are withheld from the active set and listed in the <available_tools_deferred> block, one line each: name and the description, whitespace collapsed and clipped to 140 Unicode code points (src/brain/prompt/toolCatalog.ts; personal tools are excluded from cached shared-room previews). PI's public createToolSearchExtension() searches deferred names, descriptions and parameter schemas with BM25 (src/brain/toolSearch/toolSearchTool.ts). Elowen's facade filters candidates and activation by the current sender before search and keeps the durable tool name ToolSearch; the native definition uses the common execution gate. select:Name,OtherName loads only the listed permitted tools and reports loaded, refused and unknown names without applying the search limit. Other queries pass unchanged to PI; optional limit defaults to 8. Results contain loaded names, not schema text; PI declares loaded schemas on the next model call. Journal-derived activation is filtered again for each sender and restored across respawn.
  • Anthropic hosted search. Deferred functions carry defer_loading; the core set is sent in full. The <available_tool_catalog> block names only the deferred tools, grouped by plugin. It is built only for owner chat (formatHostedToolCatalogBlock in src/brain/prompt/toolCatalog.ts), because per-sender visibility makes a static list wrong in shared channels; built-in tools form the builtin group. pi-ai puts the tools cache breakpoint on the last declared tool; when that one is deferred, the encoder moves pi-ai's own marker onto the last immediate tool, because Anthropic rejects cache_control beside defer_loading. PI keeps the initial request-level tools fixed, including its already-deferred placeholder, and sends late tools as tool_addition blocks containing a tool_definition by value. The same encoder applies the policy to every inline definition, including same-name redefinitions, without replacing historical schemas with current ones. Immediate additions and removals stay inline. A deferred inline addition cannot carry a cache marker on either its definition or its enclosing block; the enclosing history marker moves, with its original TTL, to the preceding cacheable non-deferred block. With no eligible predecessor it is dropped. No deferred tool or placeholder receives a marker. A policy-deferred function anywhere in the request, even only inline, enables the one server search tool; without one, no hosted-search projection runs. For example, a deferred MemorySearch followed by PI's placeholder moves its tools marker to immediate Read, while a late deferred plugin tool remains an inline definition discoverable by hosted search. Encoding scans the supplied tools and message blocks and makes no provider calls or extra catalog copies. The blocks Anthropic returns for a search (server_tool_use, tool_search_tool_result) have no PI home, so a replay shim keeps them on the assistant entry and puts them back verbatim on the way out. It does so only for turns the sending route wrote (provider, api and model all equal); a turn from another model goes out as pi-ai's plain conversion without them, so switching models needs no refusal and no history change. For example, a conversation with hosted searches on Opus switched to Sonnet sends those turns as text and tool calls; back on Opus, their hosted blocks return.
  • OpenAI hosted search. The deferred tools are grouped by owning plugin (groupDeferredTools) and each group is sent as one namespace whose description is the plugin manifest's description as a preview; the core set stays plain functions. pi-ai replays a call's namespace only for the model that made it, so the encoder restores it on replayed calls into a declared namespace; without it, a model switched in mid-conversation repeated the already answered call. A probed Azure deployment keeps flat defer_loading functions, because its probe proves only that shape (hostedRouteUsesNamespaces).
  • Code mode. See the next section.

The set is fixed for the life of a session, like the tool set itself: the MCP tool set changes only through a daemon restart, so neither the prompt blocks nor the exec description change under a warm prompt cache. A respawn recomputes it. The one exception is a shared room's personal tools, whose exec declaration follows the writer (see "Who a code-mode call belongs to" below).

Deferred tools in the code-mode composition request

The codeMode control's compose(request) receives request.nested: every already-gated tool a script may call. composeSessionTools marks AskUserQuestion, ExitPlanMode and ShowView as model-only before deferral, excluding them from candidates and automatic thresholds. Script exclusion consumes that exposure metadata, not a separate name filter. These tools are excluded from this catalogue and remain available as direct model tools on owner chats and platform channels. A direct question blocks the turn until an answer, cancellation, or the configured brain.limits.elicitationTimeoutMs (default 21,600,000 ms, src/store/configStore.ts); an exec observation window must not let the model continue while that question is pending. Platform adapters render and retire the core's ask / ask_resolved events without introducing another expiry clock. With no code-mode control, these tools keep their ordinary direct behavior; this exclusion adds no timers or inference cost. Each CodeModeNestedTool carries deferredPreview: string | null, the shared policy's decision for that tool: null when it stays fully declared, otherwise the same deferredToolPreview line the <available_tools_deferred> block would show. A code-mode session composes no ToolSearch, local or hosted, so this field is the only place the decision shows up. exec and wait are host-admitted to plan mode (CORE_TOOL_DEFAULTS), so a planning turn can reach nested tools as well as its direct interaction tools; each nested call still meets the plan-mode deny gate.

Core projects native constrainedSampling into CodeModeNestedTool.kind, carries the original inputSchema, and wraps a registered outputSchema in the schema of the complete returned result, including content, details and structuredContent. Native namespace instructions, or its short description when instructions are absent, become namespace.description. The existing invoke boundary normalizes omitted function arguments to {}, rejects non-object function inputs and non-string freeform inputs, and applies the freeform tool's public prepareArguments before the same gated execute function runs. Personal ownership and all existing permission gates remain authoritative. Ordinary returned errors resolve with their original isError, diagnostics and structured payload; policy refusals identified by the existing details.refusedByPolicy marker reject with the result as the error cause, preserving grant, permission and hook reasons in the shared trace. The explicit policy verdict decides refusal even when isError is false; ordinary isError alone never implies authorization refusal.

The real MCP producer passes through structuredContent, registers its structured outputSchema or an unconstrained schema when absent, and normalizes missing or null root input properties to {}. Core wraps that metadata once into the complete returned-result schema. Object content items and the optional _meta schema marker satisfy PI's public MCP detector, rendering CallToolResult<T> or CallToolResult without exposing private top-level result _meta at runtime. Ordinary MCP descriptions remain whole in code mode; Codex's 1,000-byte limit belongs only to its separate agent-plugin category. Direct-provider caps remain in core. The existing MCP declaration digest includes outputSchema, so a schema-only descriptor refresh reaches the existing reload decision without a second lifecycle path.

The existing brain.modelOverrides['provider/model'].codeMode slice preserves explicit model facts that PI's public model descriptor does not expose: use_responses_lite, messages with the CodeModeToolMessages shape, and input_schema_max_bytes. Messages allow exec.description, wait.description, wait.parameters, deferred_nested_tools_guidance and mcp_typescript_preamble. Exec always uses its fixed source schema and freeform grammar; exec.parameters is not accepted. The existing config-store normalization strips that retired member from saved model overrides before strict validation, preserving every other valid field and persisting the cleanup on the next config write. Core passes supported metadata to the route and composition request, preserving empty message overrides. modelCodeModeMetadata in the existing shared src/brain/modelCapabilities.ts authority supplies factual defaults from Codex's models-manager/models.json, pinned at 3f1ccb7ceb814e54314826f68d61c892e2f5a48e (the pin is recorded in the parity test tests/plugins/codeModePromptParity.test.ts and its fixture, not in the source file). The existing supplemental rows carry use_responses_lite: true for gpt-6-astra, gpt-6.1-sol, gpt-6-sol, gpt-6-luna, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, the blue and red gpt-daybreak-*-latest slugs, and codex-auto-review; gpt-5.5 explicitly carries false. Lookup uses upstream's case-sensitive longest slug prefix, then retries one nonempty ASCII alphanumeric/underscore/hyphen namespace with a slash-free suffix. It never uses the broader models.dev name heuristics or OAuth credentials. Explicit overrides win, including false disabling Sol's factual true default; unknown models retain only their explicit override. The lite placement gate requires the resolved use_responses_lite: true; absent messages use bundled plugin descriptions. input_schema_max_bytes reaches each nested tool's inputSchemaMaxBytes for bounded declaration rendering; it does not loosen direct-provider caps. Scripts receive full schemas and the very same gated execute function as capped direct definitions. For example, a verbose MCP schema remains available to code-mode declaration rendering while its ordinary provider declaration still takes the existing external cap. This metadata adds no provider call or runtime queue. The same native result and request seams below carry prepared media. trace.note() records user-visible progress; the separate spawner notify binding queues native model notifications.

  • Use. Keep deferred tools callable on tools.* and listed in ALL_TOOLS, without initial preview names or declarations. The literal guidance to filter ALL_TOOLS by name and description is present even when nothing is deferred. Every catalogue entry and immediate prompt declaration uses the same renderer: description, a blank line, exec tool declaration:, and a fenced declare const tools: { Name(args: T): Promise<R>; };. Freeform tools take input: string; absent input or output schemas render as unknown. plugins/code-mode/src/protocol/schemaTypes.ts replaces PI's type renderer at this one declaration seam. The default UTF-8 budget is 16,000 bytes, with a fourfold work budget, two active expansions per local-ref path and 32 total expansions. Input metadata uses max(16000, inputSchemaMaxBytes), even when the supplied budget is zero; the generic renderer still honors explicit smaller budgets. References respect nested $id resources; siblings remain intersections. Properties and literal object keys use Rust-compatible UTF-8 ordering, and comments use Unicode White_Space trimming. PI's toCodemodeIdentifier still owns dispatch names. Admit the first registered normalized collision winner before sorting the original names in the runtime catalog. Prompt sections instead sort by explicit namespace and then original name, with unnamespaced tools first. No namespace is inferred from tool-name prefixes, and namespace guidance uses Unicode White_Space trimming. outputSchema must describe the whole value returned to JavaScript, not just structuredContent. The public PI MCP detector and shared MCP_TYPESCRIPT_PREAMBLE provide CallToolResult<R> only for complete MCP envelope schemas. Namespace instructions remain model-visible alongside the tool declarations. Function/freeform validation and prepareArguments belong to core's fully gated invoke, never a second plugin validator. Optional request.messages carries upstream catalog overrides: exec.description, wait.description, wait.parameters, deferred_nested_tools_guidance and mcp_typescript_preamble. Empty and whitespace-only strings are meaningful; only {{ default_exec_yield_time_ms }} and {{ image_helper }} are substituted in the exec text. Invalid wait parameter JSON, a non-object root or malformed supported nested fields emit "Invalid catalog tool parameters; using bundled parameters" and use the bundled schema. The recursive typed parser follows upstream's JsonSchema subset: unknown fields are ignored, null option fields are omitted, primitive type names are checked and nested boolean schemas are refused. Only the root's single type "object" is accepted. Both bundled defaults are 10,000 ms; an exec operator override does not alter wait's default. Wait publishes number fields without arbitrary schema bounds and validates representable unsigned 64-bit integers before touching a cell, accepting values above JavaScript's safe range. The operator output setting is a default replaced by each explicit call budget. Pragma validation rejects all overflowing numeric JSON tokens, including overwritten duplicates, before object/key checks, then deserializes both unsigned fields before their JavaScript-safe bounds. The grammar and primary pragma diagnostics match Codex; JSON syntax and invalid-number suffixes retain JavaScript details instead of serde details. Acorn validates strict module source syntax before native execution, rejecting top-level return, with and duplicate lexical bindings; this does not add an import/export module loader. Numeric literal serialization and budgeting use the same standard shortest-decimal representation with Rust/zmij's fixed/scientific notation thresholds, exponent sign and negative zero. Pre-materialized JavaScript schema metadata cannot recover the original integer-versus-float distinction, such as JSON 1 versus 1.0; this information loss is not a general formatting fallback. Engine naming and console timing describe QuickJS-WASI truthfully. Audio, model-facing notify and image-detail transport need a larger layer above PI and remain unadvertised; generatedImage is omitted by explicit product scope.
  • When. Once per session composition, together with the session's tool set (see above). The one per-writer part is a shared room's personal tools, described next.
  • Absent. With deferral switched off, or too few tools for the threshold and no override, every tool arrives with deferredPreview: null and the description declares all of them.
  • Cost. Immediate tools carry declarations; deferred tools add no preview entries to the initial description. Their declarations are available through ALL_TOOLS. Names and declarations are sorted deterministically; descriptions contain no timestamps or per-turn values.

Who a code-mode call belongs to: principal and personal tools

A shared room composes its tools once and serves every writer, linked or not, so two per-writer facts reach the plugin as live answers rather than as composition data.

  • request.principal() returns the turn principal (turnPrincipal(currentIdentity()), the same key delegated ancestry uses): elowen:<accountId> for an account, otherwise <platform>:<senderId>. The bundled plugin keys cells, store snapshots and cell traces by session plus principal, so two unlinked room members neither share stored values nor find each other's cell on wait ("exec cell N not found"). A call with no sender identity throws instead of falling into a shared bucket. The session imposes no extra open-cell count cap. A principal session holding no open cell and no stored value is dropped when its last cell closes or a wait misses.
  • CodeModeNestedTool.visibleToWriter() is present only on one account's personal tool in a session that carries a personal-tool ownership map (a shared room; PluginRegistry.sharedRoomToolOwners). It answers through toolOwnedByOtherAccount, the predicate the visibility pass and execute gate use, for the turn running now, and false outside a turn. The plugin leaves such tools out of the registered exec description and declares them only through PI's prepareLoadout hook, which PI runs before every request inside the sending turn, and binds tools/ALL_TOOLS per exec call from the same answer. Identifier collisions are resolved within that per-writer view.
  • Absent. Owner chat, direct chats and delegated children compose one account's tools and carry no ownership map: no tool has visibleToWriter, exec has no prepareLoadout and its description is the static one, exactly as before. The execute gate still refuses a colleague's personal tool.
  • Cost. No extra calls or tokens. In a room the exec declaration changes only when the next writer owns a different personal-tool set; PI then records the changed declaration as a tool delta at the transcript tail, and the same writer keeps a byte-identical declaration.
  • Example. Amy (account 2) and an unlinked guest share a Discord room in which Amy's personal MCP server registered mcp__amy__vault. Amy's turn declares and lists it; the guest's turn neither declares nor lists it, and the guest's store('k', …) is invisible to Amy and to every other guest. tests/brain/codeModeRoomSenders.test.ts pins this through the real spawner and session factory.

Code-mode work prompt provenance

plugins/code-mode/templates/codex-work.md copies the gpt-6.1-sol model messages template from OpenAI Codex commit 3f1ccb7ceb814e54314826f68d61c892e2f5a48e, selected by Codex's longest-prefix slug rule. The identity introduction remains owned by elowen, the name uses {{agentName}}, and personaTemplatesFor (src/brain/session/personaRoute.ts) still joins elowen (PERSONA_BASE), elowen-harness (PERSONA_HARNESS) and the work template the plugin supplies through codeModeWorkTemplate; without one it uses elowen-work. No PI execution adapter is added by this prompt change.

The excluded work-template passages name unavailable Codex facilities: functions.request_user_input_async, filesystem/aliased/orchestrator skill loading, codex_apps connectors, Codex plugin skill naming and the definition of a plugin as an apps bundle. The general MCP naming, plugin trigger, capability, relevance and missing-capability rules remain. The existing Elowen harness owns native skill loading, questions and plugin capabilities. All other permission, persistence, personality, communication, orchestration and skill-policy wording is copied unchanged; trailing spaces and empty separators left by exclusions are normalized. tests/plugins/codeModePromptParity.test.ts compares the complete adjusted template against a separately pinned upstream fixture and verifies unchanged identity composition. The current Sol template has no separate file-editing-constraints section; that section belongs to other model templates and is not invented here.

Responses Lite request placement

The existing before_provider_request session extension receives the durable sessionId from the resource loader. When model metadata enables Responses Lite, it moves tools and nonempty instructions into developer input items before the existing input. Empty instructions do not suppress tools. Only function/custom tools and existing functions namespaces are grouped, at the first grouped position; hosted tools and other namespaces retain their relative order. The last nonempty default namespace description wins, using Unicode White_Space rather than JavaScript trimming so BOM remains content. Null function strict becomes false. The original top-level tools are removed, parallel tool calls are disabled, and existing reasoning gains context: all_turns.

Prefix IDs use UUIDv5: the OID namespace hashes the durable session ID, then that namespace hashes canonical tool JSON for at_ or instruction UTF-8 bytes for msg_. src/shared/canonicalJson.ts also owns registry surface equality; keys use UTF-8 ordering and array order remains significant. Retries and restored sessions preserve IDs, while content or conversation changes produce new IDs. A structural no-op check prevents duplicate prefix insertion. Non-Lite sessions do not install the extension. It adds no provider request, transcript or output queue; PI remains unchanged.

Code-mode execution protocol and limits

The bundled plugin keeps one PI QuickJS-WASI execution engine, one principal-scoped session store and one output queue. It uses only public PI APIs, never a patched worker or parsed PI error prose. There is no extra open-cell, abandoned-cell or heap guard. Observation windows are not execution deadlines; Stop, termination and session teardown really abort the native execution.

  • Script completion. The existing lexical prelude owns JSON snapshots and a dirty-key set. The existing emit bridge receives one validated completion envelope containing dirty snapshots and an optional typed script error. Dirty writes merge into the principal's current state even when authored code fails; unchanged keys never overwrite concurrent writes. Undefined/functions/ symbols are refused before replacing a value, and no native store-size cap is copied. A forcibly terminated worker cannot return that envelope. Explicit globalThis.store/load remain immutable native helpers, not the lexical session persistence path.
  • Exit and lifetime. exit throws the catchable string codex_code_mode_exit, executes finally, and completes successfully only when that requested sentinel escapes. Catching it allows execution to continue; another finally exception still fails. One pending emit lifetime capability prevents native stall detection for unresolved promises and settles when PI aborts its host signal. PendingExecution tracks all nested tools and globals and uses Promise.allSettled after PI's native result aborts them, joining cooperative cleanup before terminal delivery and sandbox disposal. Arbitrary JavaScript promises cannot be forcibly stopped through PI's public API. A handler that ignores its signal and never settles can keep cleanup pending indefinitely; there is no hidden cleanup timeout or success-shaped fallback.
  • Module environment. Acorn validates and lowers exports, dynamic import and import.meta for PI's public function evaluator. The module helper binding is selected from parsed identifiers so authored declarations cannot shadow it. Static imports and re-exports fail before authored side effects; dynamic imports reject the Codex string after native string-hint specifier coercion. Exports evaluate, but no namespace is emitted. import.meta is an empty null-prototype object. Anonymous default declarations retain their expression boundary and source lines; dynamic-import options and attribute values are checked before the unsupported loader rejects. The pinned Codex module loader rejects every import itself, so no filesystem module loader, aliases or secondary module engine is added. Authored code still runs as a strict ordinary async function with undefined this.
  • Output and timers. Each exec or wait call defaults to 10,000 output tokens (DEFAULT_MAX_OUTPUT_TOKENS, plugins/code-mode/src/protocol/output.ts), and one cell may hold at most 32 observation keys (plugins/code-mode/src/runtime/execution.ts). text serializes synchronously, throws string serialization errors and returns undefined. The same emit channel carries ready, completion, output and notifications. Ready arms the initial observation deadline after VM startup. Host observation timers use segments of at most 2^31-1 milliseconds, retaining the same deadline and observer across segments. Completion, yield_control, cancellation and disposal clear the active segment. An image reserves its position before asynchronous decode; the observer claim remains held until that batch drains. Each exec/wait has one independent formatting and truncation pass, including artifact references and generated errors. Authored item indexes and retained UTF-8 spans keep memory admission separate from generated notices. Images and audio do not spend text tokens. Audio, notify and settlement helpers expose their same cell-local function on globalThis, without proxies or duplicated implementations. console and explicit globalThis.text/image stay PI-native completion-only output and retain PI's own caps. Timer callbacks stay in the cell, receive no extra arguments, fail the script on errors and do not keep finished code alive. sleep honors its requested duration and is cancelled only by genuine termination. Observation requests of at least ten seconds receive one second of grace without a configured ceiling. as_settled/stream_settled observe inputs once, retain settlement order and rejected reasons, preserve Map keys and await callbacks.
  • Late trace settlement. ToolTraceSettlements reads the reporting tool result by call/row from the stored journal independently of retained live entries. It writes the durable result before mutating any live copy, and required write failure poisons the one PersistenceFailureLatch already used by the session's provider guard. Missing/discarded entries remain removed; no transcript is appended.
  • Notify. notify serializes like text and returns undefined synchronously. Empty Unicode whitespace is refused before the host bridge. CodeModeCompositionRequest.notify captures the cell principal; its boolean acceptance checks the session's current native turn principal and policy lifetime, never the old cell's AsyncLocalStorage identity. False retains the notice in the existing principal-keyed CodeModeSession. Native agent_start calls CodeModeControl.resumeNotifications for that principal only. Cell completion/session shutdown explicitly drops and logs undelivered notices. A missing model hook leaves progress display-only; with no sink at all notify fails explicitly. Accepted notices use native Agent.steer with the original exec toolCallId, toolName exec and details.codeModePrincipal. The existing core/transcript source omits other principals' notices from request copies, without modifying native history. PI owns delivery and journaling; the main output budget does not charge notifications.
  • Prepared media. plugins/code-mode/src/runtime/imageOutput.ts supplies the single normalization function embedded in the prelude, before the JSON bridge can erase getters or values. image accepts data URLs, ordinary image_url objects with detail and individual MCP image blocks, including MIME aliases and metadata. It reads ordinary URL getters once, ignores unrelated properties and throws synchronous string errors. Explicit detail overrides embedded detail. Actual decode failures produce the Codex omission notice without failing authored code. Public resizeImage decodes and applies the high dimension/patch budgets 2048/2500 or original 6000/10000 with 32-pixel patches. Low detail produces the upstream omission notice. Original is enabled only by supportsOriginalImageDetail on CodeModeCompositionRequest, supplied from the existing model metadata's supports_image_detail_original fact or explicit override; absent/false becomes high. The pinned experimental UnifiedImageBudget feature is disabled by default and is not enabled by this plugin. Native global image output receives high detail in the same adapter. audio normalizes data URLs and individual MCP audio blocks. PCM/float WAV shorter than 25 ms is omitted using present frame bytes, not declared streaming length; unknown formats remain media. The shared module packages/plugin-shared/media.mjs (imported as elowen-plugin-shared/media) owns canonical bounded data-URL decoding: 10 MiB decoded per call, 20 MiB per exec/wait observation (MAX_OBSERVATION_MEDIA_BYTES) and a 256-character header. Admission happens before image decode and includes native terminal image output. Async queue draining independently verifies prepared bytes. The prelude joins pending output calls before committing completion, including exit; size failures cannot race with a success envelope. No generic cell, heap or abandonment guards are restored.
  • Provider seam. The native result's details.providerMedia v1 preserves ordered prepared image/audio URLs and image detail. Text entries refer to native content text-block indexes, never duplicate text. Native image content is the existing display/history projection, which PI may resize again; prepared media bytes in live result details remain the authority for the Responses wire projection. At the existing externalizeImageBlocks append barrier, details.providerMedia URLs become typed image and audio references in the existing content-addressed chat-images/chat-files stores. Bounded original URL prefixes preserve exact prepared URLs during restoreToolMediaDetails on runtime copies. Prepared attachment write/read failure is explicit, never inline fallback or journal repair. Persisting prepared media without an attachment root fails through the existing persistence latch. collectImageFiles, collectChatFiles and the shared attachmentSql file-reference predicate cover these references for both ownership and the daily sweep; db.ts rebuilds its existing partial index from that same predicate. PromptComposerRuntime extends its existing onPayload wrapper, including requests without hosted deferral. Without media, native payload bytes and downstream options stay unchanged. Projection runs after catalog/hosted replay projection and before downstream replay/cache inspection. It validates media descriptors and data-URL protocols and projects input_text/input_image/input_audio into the corresponding function/custom tool-call output without another transcript or transport. Call IDs come from the existing request's ordered calls, not a parallel sanitizer. Public PI transformMessages supplies orphan results and nonvision image downgrades; no transformed history is persisted. Missing annotated results fail explicitly. Repeated result occurrences preserve notify outputs sharing the same exec ID. Cleared results use current native text, not original output, and images retired from native history are never resurrected. Codex Responses carries audio_url; other provider APIs explicitly refuse tool audio. Responses APIs preserve image detail; other APIs keep their native image encoding because they have no equivalent Responses detail field. Results without the descriptor remain unchanged.
  • Public-API limits. PI's ImageResizeOptions exposes maxWidth, maxHeight, maxBytes and jpegQuality, but no sampling-filter, exact-size, forced-codec or color/EXIF-metadata control. Its processor uses Lanczos3 and PNG/JPEG encoding, whereas Codex uses Triangle, exact target dimensions and retained RGB ICC/EXIF metadata. Unresized supported bytes can be preserved, but exact resized pixels, GIF conversion and metadata/codec parity cannot be obtained from that API. This is an observed API boundary, not an owner-deferred feature. Native immutable globals, Intl/ICU behavior and Codex's internal frontier interface remain engine/API differences. generatedImage stays excluded by scope.

With the plugin disabled there is no code-mode composition or sandbox execution. The existing provider opt-in and Elowen's tool-security boundary remain the authority; PI settings are not another writer.