Extension wire protocol reference

Feature-negotiated JSON-RPC services reused by API 0.4; distinct from native-host protocol 1.

Manual for v0.9.5. Stable release: v0.9.5.

Preserved reference URL; not part of the current manual navigation.

Identity boundary: octet 0.8.0 source uses only octet first-party names, including octet_version, requires_octet, OCTET_*, and octet_extension. Retained API numbers 0.1/0.2/0.3/0.4 do not imply aliases for old Ygg wire names or imports. The source SDK distributions and four official executable bundles are version 0.8.0; independent examples keep their own versions. Native assets and public-install verification are recorded in the v0.8.0 GitHub release. Native publication does not publish SDK registries.

Version scope: API 0.4 is current and uses the feature-negotiated wire retained from supported API 0.2; API 0.1 stays frozen. Versioned examples below retain their original IDs. API 0.3 is a distinct canonical wire in the generated reference; its deferred entries do not remove dynamic tools, presentation, or child sessions from the feature-negotiated wire. These low-level contracts and safety tests remain live, but only an actual host offer and frontend binding make a service available. Pi mappings below are historical implementation notes, not a shipping bridge or a release gate.

Every request and response uses the standard JSON-RPC 2.0 envelope with exactly one JSON object per line on stdout. Human diagnostics belong on stderr, which octet drains and exposes as bounded diagnostic events.

Extensions send process-to-host messages at any time after initialization. For graceful shutdown, the host sends a JSON-RPC shutdown request; the extension acknowledges it and exits promptly. Stdin EOF means the transport was lost or finally torn down and should also make the extension exit.

The manifest selects exactly one wire version. API 0.1 remains available, with its frozen initialization structure in the octet namespace, for trusted text-oriented extensions. It does not gain API 0.2 cancellation, progress, structured/media retention, parent-correlation, or terminal lifecycle guarantees. API 0.2 adds those stateful guarantees through explicit initialization negotiation; support is never inferred from the extension package version. Installable bundles also carry an exact requires_octet requirement in extension.toml; it is validated before a process can start and is packaging metadata, not an initialization field or protocol-version substitute.

This protocol supports bounded integrations in a deliberately small coding host. octet hosts model conversations, session/result persistence, permissions and approvals, process supervision/cleanup, and resource limits. MCP, browser use, computer use, web search, memory, LSP, subagent orchestration, and caffeinate are examples of separate domains, not a promise to deliver a package for every capability. Generic host services support these integrations without moving their domain protocols into the host.

Implemented limits in this reference are protocol, queue, concurrency, artifact, timeout, and process-tree cleanup bounds. octet does not yet enforce OS CPU/RSS/FD/PID quotas or sandbox trusted extensions; they run with the current user's authority.

Capability declarations decide what services the host routes and what requests an extension may make; they do not confine the process. OS-level enforcement is future work.

Transport defaults#

Parameter Default
Max JSON line 1 MiB (DEFAULT_EXTENSION_MESSAGE_BYTES)
Host in-flight requests 64 (DEFAULT_PENDING_REQUESTS); API 0.2 may negotiate lower
Complete frames waiting for the serialized writer 128 (DEFAULT_WRITER_QUEUE)
Request timeout 30 s
API 0.2 cancellation grace 2 s
Cancelled-ID tombstone retention 30 s, at most 512 IDs
Normal shutdown request/ack stage 2 s (ExtensionRuntimeConfig::shutdown_timeout)
Normal post-request process-exit stage 2 s (the same per-stage timeout)
Normal product aggregate shutdown 3 s (all extension shutdowns run concurrently)
Coordinated-signal extension-shutdown cap 1.4 s in interactive, plain, print, and host modes; then force-kill registered process groups

Product discovery reads a selected extension manifest through the resource resolver's 256 KiB bound. The lower-level ExtensionManifest::load API instead defaults to 64 KiB; the product discovery path calls parse on the resolver's bounded text and does not use that lower-level default.


The host uses one bounded writer to serialize complete frames onto the child's stdin; the SDK likewise gives one writer sole ownership of extension stdout. Neither abandons a partially written frame. API 0.2 requests are also admitted through the negotiated concurrency semaphore.


1. Host-to-extension methods#

1.1 initialize#

The first host request, sent immediately after the child process starts.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "api_version": "0.1",
    "octet_version": "0.8.0",
    "extension": {
      "name": "hello-world",
      "version": "0.1.0",
      "manifest_path": "/home/user/.octet/extensions/hello-world/extension.toml",
      "source": "global"
    },
    "workspace": "/home/user/project",
    "capabilities": {
      "filesystem": "none",
      "process": false,
      "network": false
    },
    "contributes": {
      "tools": ["hello_world"],
      "commands": ["hello"],
      "hooks": ["before_prompt", "after_response"],
      "ui": ["status"],
      "context": true,
      "tool_renderers": ["hello_world"],
      "notifications": true,
      "confirmations": false
    },
    "host": {
      "session_id": null,
      "session_name": null,
      "model": "claude-sonnet-4-6",
      "reasoning": null,
      "active_skills": []
    }
  }
}

API 0.1 response:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "api_version": "0.1",
    "tools": [
      {
        "name": "hello_world",
        "description": "Greet someone from an executable extension",
        "parameters": {
          "type": "object",
          "properties": {
            "name": { "type": "string" }
          },
          "required": ["name"],
          "additionalProperties": false
        }
      }
    ],
    "commands": [
      {
        "name": "hello",
        "description": "Show a greeting notification",
        "usage": "/hello [name]"
      }
    ]
  }
}

Common validation rules:

  • api_version in the response must exactly match the manifest-selected version, or the child is rejected.
  • Without negotiated discovery features, the duplicate-free sets of tools and commands returned must exactly equal the corresponding names declared in contributes. Negotiated dynamic_tools makes initialize's tool list authoritative epoch 0; negotiated runtime_commands makes initialize's command list authoritative for that fixed process generation. Duplicate names always reject initialization, and an initialize response may contain at most 256 command definitions.
  • Each tool must have a non-empty description and parameters must be a JSON Schema object.
  • source is one of "project", "global", or "explicit".

API 0.1 uses the request and response above and must omit protocol from both. A 0.1 tool definition cannot declare output_schema, and a 0.1 manifest cannot declare semantic presentation.

API 0.2 retains every top-level initialization field, may set contributes.presentation to true, and adds this request member:

json
{
  "protocol": {
    "version": "0.2",
    "required_features": ["request_cancellation", "content_parts"],
    "optional_features": [
      "request_progress",
      "artifacts",
      "lifecycle_events",
      "policy_intents",
      "dynamic_tools",
      "runtime_commands"
    ],
    "limits": {"max_concurrent_requests": 64}
  }
}

The response must retain top-level api_version, tools, and commands, and add:

json
{
  "protocol": {
    "version": "0.2",
    "features": [
      "request_cancellation",
      "content_parts",
      "request_progress",
      "artifacts",
      "lifecycle_events",
      "dynamic_tools",
      "runtime_commands"
    ],
    "limits": {"max_concurrent_requests": 4},
    "lifecycle_events": [
      "session/started",
      "session/settled",
      "turn/started",
      "turn/settled",
      "tool/started",
      "tool/settled"
    ]
  }
}

request_cancellation and content_parts are required. The response may add any subset of the advertised optional features, but missing required, unknown, or duplicate feature names reject initialization. The accepted max_concurrent_requests must be greater than zero and is capped by the host. runtime_commands is initialization-only: the returned command list is fixed for the generation, there are no commands/register or commands/unregister methods, and reload still requires an identical command catalog. It exists for compatibility runtimes that cannot know foreign command names before loading that runtime.

If lifecycle_events is negotiated and the subscription list is omitted or empty, all six events are subscribed. Otherwise it must be an exact subset of the six names above. A non-empty subscription without the feature is invalid.

API 0.4 also offers tool_prompt_metadata_v1. A tool may then declare prompt_snippet (optional string) and prompt_guidelines (optional string array, default empty). These are presentation only: at most 16 guidelines and 1,024 UTF-8 bytes per string, allowing newline/tab but no other control characters. Their encoded bytes share the existing aggregate catalog schema budget. Initialization and dynamic registration use the same validation. The Agent projects only active, model-visible tools; guidelines need no snippet, and ordinary no-metadata tools remain unchanged. See prompt projection and run snapshot semantics.

The API 0.4 host may additionally offer tool_composition_v1 when its runtime explicitly enables ExtensionRuntimeConfig.tool_composition (false by default for embedders). The coding host enables this generic service for trusted enabled API 0.4 extensions, not just one package name. Negotiation alone conveys no authority: only a live model-tool request with a bound composition dispatcher can use it. See composition.

API 0.4 offers session_control_v1 only with a bound active-session lifecycle service. session_compaction_v1 additionally requires that service to opt into a real idle compaction consumer; selecting it requires session_control_v1. Admission still requires an active frontend binding. Neither feature changes canonical API 0.3 session_lifecycle. See session control and compaction for the distinct parent-lifetime rules.

The coding host conditionally appends agent_sessions to optional_features only for the trusted, enabled first-party octet-subagents extension when its child-session service can be bound. The service is available independently of the selected reasoning effort; Ultra is separately gated on the live provider's V2 metadata. A response may negotiate it only when it was offered. The service is bound after the Agent is constructed; calls without a bound service/resource owner fail deterministically with -32002. When agent_sessions is offered, the host also offers agent_model_selection_v1; negotiating the latter requires agent_sessions. API 0.4 additionally offers agent_session_events_v1 and agent_session_lifetime_v1 at that same authorized service boundary; each requires agent_sessions. These features do not enable child authority for Pi factories or other packages in the coding host. Embedders must explicitly bind the native child service; feature names alone grant nothing. The model-selection feature enables bounded configured-model discovery and explicit child model selection, not extension-supplied provider transports or credentials.

The host likewise appends approvals only when single-use approval issuance is enabled, and appends secrets only when a secret broker is configured and the manifest's exact [capabilities].secrets allowlist is non-empty. Negotiating approvals also requires policy_intents; neither conditional service may be returned when it was not offered. The coding product currently leaves approvals disabled and configures no secret broker, so it offers neither conditional feature. Generic policy/evaluate requests return deny; the working-tree mcp.tool.call adapter permits exact active owner-scoped MCP calls under full access only, as described below.

API 0.2 may also declare [capabilities].provider_credentials = true. The reviewed declaration is the only source of the conditional provider_credentials feature: a manifest that omits it can never negotiate the feature, even if the process advertises it during initialization, and a declared manifest echoes provider_credentials: true in the initialize capabilities object. Negotiating it admits provider/credentials, which resolves exactly one named native provider/model through the host's own authentication resolution and returns that single result to the requesting process. The value reaches neither diagnostics nor status nor persistence, and a request for any other identity is refused rather than widened.

Secret names are duplicate-free identifiers of at most 64 ASCII bytes. The first character is a letter or underscore; subsequent characters may also use digits, hyphen, or dot. The list is a broker allowlist, not launch-environment injection, and exact undeclared names remain inaccessible. An empty allowlist is omitted from the serialized capabilities object, preserving the literal API 0.1 initialize shape above. A non-empty current/API 0.2 manifest includes the exact secrets array in that object.

API 0.2 may also include [capabilities].environment, an explicit ambient broker allowlist. The only current reviewed name is SSH_AUTH_SOCK. It remains absent from the default sanitized subprocess environment and is copied from the host only when declared and present; values are not included in initialize, diagnostics, or persistence. Unsupported names and any API 0.1 declaration are invalid. Access to an agent socket grants signing authority to the trusted extension and is not a sandbox.

API 0.2 tool definitions may add output_schema, a bounded supported subset of JSON Schema used to validate structured_content. Schema nodes must be objects, nesting is capped at 32, property names at 256 bytes, and the accepted keywords are $schema, title, description, default, examples, type, properties, required, additionalProperties, items, enum, const, allOf, anyOf, oneOf, minimum, maximum, exclusiveMinimum, exclusiveMaximum, minLength, maxLength, minItems, maxItems, uniqueItems, minProperties, and maxProperties. Arbitrary JSON Schema vocabulary is rejected.


1.2 tool/call#

Invoke a model-callable tool.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tool/call",
  "params": {
    "name": "git_status",
    "arguments": {
      "include_ignored": false,
      "max_entries": 80
    },
    "catalog_revision": 2,
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "resource_owner": {
        "session_id": "durable-session-owner",
        "extension_instance_id": "host-created-instance-fence",
        "process_generation": 3
      },
      "host": {
        "session_id": "abc123",
        "session_name": null,
        "model": "claude-sonnet-4-6",
        "reasoning": null,
        "active_skills": []
      }
    }
  }
}

catalog_revision is present only for API 0.2 extensions that negotiated dynamic_tools. It selects the exact per-process catalog epoch the model saw. An extension must dispatch against that historical schema-and-handler snapshot, not whatever catalog happens to be newest when the call arrives. API 0.1 and API 0.2 without dynamic_tools omit the field.

resource_owner is present for API 0.2 model-tool/tool-hook contexts and, in the coding product, slash commands, before_prompt, after_response, and context/collect. Its host-derived session_id is the durable namespace for extension state. extension_instance_id changes across a complete process-host rebuild, even when generation numbering restarts, and process_generation rejects stale browser tabs, MCP/LSP connections, memory handles, and comparable resources after an extension reload or automatic restart within that host instance. Key state by all three fields. API 0.1 omits the field. Status, renderer, and unsolicited contribution contexts remain process-scoped and must not allocate session-owned handles. A context owner alone does not authorize reverse host services: model-tool and declared-command requests are active parents, while prompt/context handlers are not, and the negotiated service rules still apply.

API 0.1 response:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": "branch=main\nstate=clean\ncounts=staged:0,modified:0,untracked:0,ignored:0,conflicted:0",
    "is_error": false
  }
}

Fields:

  • content — compact model-visible result text (string).
  • is_error — if true, the result is treated as a tool error.
  • metadata — optional JSON accepted by the API 0.1 decoder for compatibility. The current subprocess adapter discards it while constructing native ToolOutput; it is not sent to the model and has no frontend, renderer, or persistence guarantee.

API 0.2 response:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {"type": "text", "text": "Found 3 sources."},
      {
        "type": "image",
        "artifact_id": "artifact-opaque-id",
        "mime_type": "image/png",
        "alt": "Search result preview"
      }
    ],
    "structured_content": {
      "sources": [{"title": "Example", "url": "https://example.com"}]
    },
    "is_error": false,
    "metadata": {"cache": "miss"}
  }
}

API 0.2 content is a non-empty ordered array of at most 256 parts and must contain at least one explicit text part. Supported parts are:

  • { "type": "text", "text": string }
  • { "type": "image", "artifact_id": string, "mime_type": string, "alt"?: string }
  • { "type": "audio", "artifact_id": string, "mime_type": string, "transcript"?: string }

Image and audio parts require the artifacts feature and must name a verified artifact from the active host-derived session owner and process generation whose MIME type and media kind match the part. Repeated references count toward a 64 MiB aggregate referenced media bound per result. If the tool declares output_schema, structured_content is required and validated; without an output schema it is forbidden. Structured content and bounded metadata are retained in native result details. Structured content is lowered to the model only by explicit host policy; metadata remains non-model-visible. Text and verified media use the normal native tool-output path. Structured content is bounded to 256 KiB; metadata to 64 KiB; both are capped at 32 levels and 16,384 nodes, and metadata keys are at most 256 bytes.


1.3 command/execute#

Invoke a slash command (/mycommand).

Request:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "command/execute",
  "params": {
    "name": "checkpoint",
    "arguments": ["release-v2"],
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

Response:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "text": "Checkpoint preview · release-v2\nmain · clean\n\n...",
    "notifications": [
      {
        "level": "info",
        "title": "Read-only checkpoint",
        "message": "No commit or filesystem mutation was performed."
      }
    ],
    "context": []
  }
}

1.4 hook/run#

Invoke a lifecycle hook (declared in contributes.hooks).

Request:

json
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "hook/run",
  "params": {
    "hook": "before_prompt",
    "payload": { "prompt": "the user's current input text..." },
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

Both legacy APIs support "before_prompt", "after_response", "before_tool_call", and "after_tool_call". API 0.2 additionally supports typed provider_retry, before_persistence, and post_mutation hooks. See retry advice and bounded metadata and rescan enrichments for their payloads, non-veto boundaries, and current product coverage.

payload is hook-specific:

  • before_prompt: { "prompt": string }
  • after_response: { "response": string }
  • before_tool_call: { "name": string, "arguments": { ... } }
  • after_tool_call: { "name": string, "arguments": { ... }, "output": string, "is_error": bool }

Both API versions invoke after_response only after a successful, complete assistant response. API 0.2 uses it as a bounded content synchronization hook; it does not replace terminal lifecycle observations and is not invoked for failure, cancellation, interruption, frontend loss, or shutdown.

Response:

json
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "disposition": { "action": "continue" },
    "context": [
      {
        "label": "local-model-workflow",
        "content": "Local-model workflow is active...",
        "placement": "system_suffix"
      }
    ],
    "notifications": []
  }
}

Dispositions:

  • { "action": "continue" } — proceed normally.
  • { "action": "deny", "reason": "..." } — deny the intercepted operation (meaningful for the legacy before_prompt and before_tool_call; other legacy hooks continue regardless). The awaited session-operation hooks below have their own decision validation, not this ignored-denial rule.

1.4b Native model-turn hooks (API 0.4, feature session_entries)#

Source status: these newly authored native paths and tests are not yet compiled or native-test-qualified. This specifies the source contract, not an installed-binary or full Pi compatibility claim.

For these model-turn and compaction contracts, an unsupported/safe refusal is a boundary, not repair evidence for a captured Pi breakage. Captured Pi breakages remain Octet-owned open defects; refusal tests do not establish closure.

Declare model_turn_start, model_turn_end, or both in contributes.hooks, select API 0.4, and negotiate session_entries. They use SessionOperationHook, registered through ExtensionHost::session_operation_hook, with a real awaited private session-leaf consumer. They are not API 0.3 session_start/session_end, best-effort lifecycle notifications, whole-run turn/started/turn/settled, or AgentEvent::TurnFinished aliases.

The host sends ordinary hook/run requests with params.hook set to the declared name. The respective params.payload shapes are:

json
{"kind":"model_turn_start","run_id":"run:001","turn_index":0,"timestamp_ms":1791129600000}
json
{"kind":"model_turn_end","run_id":"run:001","turn_index":0,"timestamp_ms":1791129600123,"assistant_entry":{"id":"002","parent":"001","timestamp_unix_ms":1791129600110,"value":{"type":"message","Assistant":{"content":[{"Text":"Done."}],"model":"local-model","protocol":"open_ai_chat"}}},"tool_result_entries":[]}

IDs and times above illustrate shape; the host uses actual session facts. run_id is run:<initiating durable user entry id>. turn_index is zero-based within that run, one logical iteration per assistant response. timestamp_ms is host wall-clock Unix milliseconds at the respective boundary. Start is awaited before model-request preparation; provider retries, context-compaction re-entry and auxiliary compaction/gate calls do not create duplicate starts or additional logical turns. The hooks themselves do not make model calls.

End is awaited only after one real assistant entry and all its paired tool-result entries have committed, including tool errors and asynchronous tool settlement. tool_result_entries is in durable commit order. Asynchronous tools may overlap a later model iteration; end observations remain in iteration order, not provider completion order. End does not mean whole-run success or terminal-gate approval.

The entries are native Entry serializations, unchanged except for the existing receiving-namespace metadata filter: retain public extension metadata and the receiver's own private metadata, not another extension's private values. Keep real IDs, parents, timestamp_unix_ms, optional metadata and all content. Native assistant content is under value.Assistant; tool results are under value.User.content[].ToolResult, potentially several in one entry. No usage, stop reason, provider-specific or Pi message fields are fabricated. An adapter unable to project media or opaque content must refuse explicitly, not drop it.

The callback includes the existing owner-fenced session snapshot and private params.session_leaf grant. While it is awaited, the sole Session writer services the authenticated, bounded private session/append_entry lane and returns real commit receipts; a frontend notification queue is not its consumer. Grants are invocation-scoped, with activation/operation, owner, instance and generation fences, and are revoked when the invocation settles. Retaining the snapshot does not retain append authority or exempt ordinary reverse calls from their parent lifetime. Full snapshot/frame bounds still apply; oversize fails, not truncates.

Return the ordinary permitted hook envelope, optionally adding "session_operation":{"action":"continue"}; absence means Continue:

json
{"disposition":{"action":"continue"},"notifications":[],"session_operation":{"action":"continue"}}

Both observations reject session_operation actions cancel and replace_compaction, and reject disposition deny. Unrelated hook effects and nonempty prompt-context contributions are invalid. Callbacks run in registration order under the shared cancellation/deadline boundary. Failure or cancellation stops the run but cannot undo committed assistant, tool-result or private entries. Callbacks are not retried and no duplicate end is synthesized. Preparation, persistence or cancellation failure may leave a start without an end; parked or separately deferred-resume polling is not represented as a fabricated completion.


1.5 context/collect#

Host requests prompt context contributions.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "context/collect",
  "params": {
    "prompt": null,
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

Response:

json
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": [
    {
      "label": "hello-world",
      "content": "The hello-world extension is active.",
      "placement": "system_suffix"
    }
  ]
}

placement is one of: "system_prefix", "system_suffix", "prompt_prefix", "prompt_suffix" (default).


1.6 status/collect#

Host requests an optional status/header/footer contribution. The coding TUI intentionally does not request or render these as persistent chrome; the method remains protocol vocabulary for other host-owned frontends.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "status/collect",
  "params": {
    "surface": "status",
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

surface is one of: "status", "header", "footer".

Response:

json
{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "surface": "status",
    "text": "hello",
    "style_role": "extension.hello_world.status",
    "priority": 0
  }
}

Return null to contribute nothing.


1.6b menu/collect (API 0.2+)#

Requires contributes.menu = true and at least one declared command. The coding TUI sends it when the person selects the extension under /extensions, and again after every action, so the menu is pulled fresh and never pushed. Keep the handler fast and side-effect free: answer from cached state. The host waits at most 5 seconds and then falls back to one generated entry per declared command.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "menu/collect",
  "params": {
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {},
      "resource_owner": {
        "session_id": "session-…",
        "extension_instance_id": "…",
        "process_generation": 1
      }
    }
  }
}

Response: the complete ExtensionMenu:

json
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "title": "Computer use",
    "status": {"state": "pending", "label": "Not set up"},
    "detail": "Set up installs Cua Driver and checks what your desktop needs.",
    "items": [
      {"id": "setup", "label": "Set up computer use", "command": "computer-use",
       "arguments": ["setup"], "recommended": true,
       "description": "Install the driver, then check permissions"},
      {"id": "jev", "label": "Jev (optional)", "items": [
        {"id": "forget", "label": "Forget stored API key", "command": "computer-use",
         "arguments": ["jev", "forget"], "destructive": true}
      ]}
    ]
  }
}

Each item is either an action (command names one of the extension's declared commands, arguments are literal) or a submenu (items). Choosing an action runs command/execute with those arguments, so a menu can only run what the extension could already run. The host asks for confirmation before a destructive action and then pre-approves that action's own first confirmation/request. While the action runs, its $/progress status lines, confirmation/request, and input/request appear in place, and the person can cancel it. Because a person started it and watches it, the action may run for up to 30 minutes instead of the ordinary request deadline.

The host rejects the whole menu (and shows generated entries instead) when an action routes to an undeclared command or a bound is exceeded: at most 256 items across all levels, 4 levels, unique ids per level, at most one recommended item per level, and a submenu that carries neither arguments nor destructive.


1.7 tool/render#

Host requests semantic renderer output for a tool call.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tool/render",
  "params": {
    "name": "git_status",
    "arguments": {},
    "output": "branch=main\nstate=clean\n...",
    "is_error": false,
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

Response:

json
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "segments": [
      { "text": "git · clean", "style_role": "extension.git_tools.clean" },
      { "text": "\n", "style_role": null },
      { "text": "...", "style_role": "extension.git_tools.detail" }
    ]
  }
}

1.8 shutdown#

Sent when the host wants the extension to exit gracefully.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "shutdown",
  "params": {}
}

Response:

json
{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {}
}

On normal shutdown, the host first waits shutdown_timeout (2 seconds by default) for the JSON-RPC reply. Whether the request is acknowledged or times out, it then waits up to the same per-stage timeout for the child to exit. If the child does not exit, octet terminates the child process group. Normal product shutdown runs these per-connection sequences concurrently inside a separate 3-second aggregate deadline; dropping a remaining connection also terminates its process group.

Interactive, plain, print, and host coordinated-signal exits instead cap the whole extension-shutdown attempt at 1.4 seconds, then force-kill all registered process groups. That outer cap may cut either normal 2-second stage short. Closing stdin is not the graceful shutdown signal; it is a transport-loss/final-teardown fallback.

API 0.2 enters draining before shutdown: it admits no new work, cancels remaining requests at the bounded deadline, then performs the same bounded request/ack, exit, and process-group cleanup sequence. API 0.1 keeps its legacy resident-process behavior.


1.9 $/cancelRequest (API 0.2)#

The host cooperatively cancels a host request, or an extension-originated child request owned by it:

json
{
  "jsonrpc": "2.0",
  "method": "$/cancelRequest",
  "params": {"id": 42, "reason": "user"}
}

Before the serialized writer starts the original frame, cancellation skips the frame and sends nothing. Once writing has begun, that complete frame is sent, followed by at most one cancellation notification. A cooperative extension observes its request token and answers the original ID with JSON-RPC error code -32800; a normal result may instead win the race.

The host drops the cancelled waiter, cancels its unresolved child requests, and tombstones the ID. A late response to a tombstone is ignored and diagnosed without closing unrelated calls. If the extension does not settle within the 2-second default grace period, its generation becomes degraded and is terminated. Cancellation requests cooperation; it does not imply rollback of external side effects and an ambiguous unsafe operation is never replayed.


1.10 Lifecycle notifications (API 0.2)#

When lifecycle_events and the specific method are subscribed, the host sends best-effort JSON-RPC notifications named:

  • session/started, session/settled
  • turn/started, turn/settled
  • tool/started, tool/settled

For example:

json
{
  "jsonrpc": "2.0",
  "method": "turn/settled",
  "params": {
    "session_id": "abc123",
    "run_id": "extension-run-7",
    "turn_id": "extension-turn-7",
    "outcome": "completed",
    "duration_ms": 942,
    "reason": null
  }
}

Start events carry their stable session/run/turn identifiers. Tool events also carry tool_call_id and tool_name. Settled events add outcome, duration_ms, and an optional reason bounded to 4 KiB UTF-8. Outcomes are completed, failed, cancelled, interrupted, frontend_disconnected, shutdown, or limit_reached. These observations are non-veto and bounded: product session/turn delivery uses a 250 ms deadline and tool observations use the bounded writer queue. Host finalizers remain authoritative if delivery fails.

after_response remains success-only in both API versions. It is not sent on failure, cancellation, interruption, frontend loss, shutdown, or a turn limit; API 0.2 lifecycle settlement covers those terminal outcomes. Root-product turn/started and turn/settled describe the owning run, not each model iteration; use the awaited API 0.4 model-turn hooks above for durable per-iteration facts.

1.11 Wave-1 notifications (API 0.2, negotiated)#

These notifications are additive and optional. Each is gated by one negotiated feature; the host emits it only after initialize accepted that feature, and an extension that never negotiated it sees nothing. Every payload is a bounded plain object: the frame is a JSON-RPC notification (no id), and an unnegotiated method, a non-object payload, or an oversized payload is refused with a bounded diagnostic instead of being delivered.

Notification Feature Pi surface
shortcut/trigger shortcuts Dispatches the admitted runtime shortcut whose id matches params.id
message/started lifecycle_events_v2 message_start
message/updated lifecycle_events_v2 message_update
message/settled lifecycle_events_v2 message_end
compaction/started lifecycle_events_v2 Advisory start only; not the awaited session_before_compact hook
compaction/settled lifecycle_events_v2 Advisory settlement only; not the awaited session_compact hook
compaction/failed lifecycle_events_v2 session_compact_failed
session/info_changed lifecycle_events_v2 session_info_changed
dialog/started lifecycle_events_v2 ui_prompt_start
dialog/settled lifecycle_events_v2 ui_prompt_end
model/selected lifecycle_events_v2 model_select
reasoning/selected lifecycle_events_v2 thinking_level_select
bash/user lifecycle_events_v2 user_bash
terminal/grant-lost terminal_handoff Revokes a ceded terminal grant: the holder must stop owning the tty and restore its own state

shortcut/trigger carries {"id": "<shortcut id>"}. The id must match a shortcut whose shortcut/register was admitted; a trigger for an unknown, refused, or stale registration fails closed and never runs a handler.

message/updated carries exactly one coalesced host batch: {"message_id": "<bounded id>", "delta": "<coalesced text>", "deltas": <count>} — delta is the coalesced text and deltas is how many extension-visible deltas it represents (a batch is emitted at 4096 accumulated bytes, 64 deltas, or 50 ms, whichever comes first, and always before message/settled). The bridge forwards each batch once, in arrival order, as a single message_update event: it never splits, re-buffers, or reorders it and never opens a per-delta round trip. It also accepts an SDK-shaped batch as a JSON array under deltas (or the alias updates); an array longer than 1024 deltas is refused as bounds_exceeded and no event is emitted. The API 0.4 session_before_compact and session_compact callbacks instead use declared, awaited SessionOperationHook dispatch with negotiated session_entries and a real private append consumer. The before-hook intercepts prepared local compaction; the after-hook observes its actual durable entry. Advisory compaction/started/compaction/settled notifications cannot provide these semantics, and the adapter does not re-emit them as duplicate callbacks. Other Wave-1 mappings above retain their bounded dispatch/refusal behavior.

These notifications do not change the extension API version. They remain API 0.2; there is no API 0.3 variant, and API 0.3 provider manifests do not negotiate any of them.


2. Extension-to-host messages#

Extensions send these after initialize completes. API 0.2 operation-scoped requests (confirmation/request, input/request, artifact/publish, policy/evaluate, and secret/get, plus every agent/* method) must include the active numeric host parent_request_id.

Optional Wave-1 resource_owner#

The ten Wave-1 requests (composer/get, composer/set, composer/insert, shortcut/register, session/append_entry, session/set_name, session/set_label, session/send_message, session/send_user_message, tools/set_active) additionally accept one optional resource_owner object: { "session_id": "...", "extension_instance_id": "...", "process_generation": 3 }. It is additive: older hosts ignore it, a request without it stays valid, and no feature or API version changes because of it.

Precedence is fixed. If parent_request_id names a live host request, that request's resource owner wins and the explicit field is ignored. If the parent request has already settled — a deferred caller such as a later HTTP callback — an explicit resource_owner that validates (matching process generation, the instance id issued to this process, and a session id the host already issued) is admitted and answered independently of the settled parent. Anything else, any foreign or stale owner, or an absent field with a settled parent is refused with -32002 not_foreground_owner. An invalid owner is never coerced into a valid one, and no other Wave-1 request admits a caller-supplied owner. Global notifications, context, status, and presentation contributions, plus process-scoped tool-catalog mutations, do not include it. When the parent settles, the host cancels every unresolved operation-scoped child and ignores late child replies.

Every extension-originated request ID is either an unsigned 64-bit integer or a string of at most 256 UTF-8 bytes. IDs are unique among outstanding child requests and cannot be reused within a process generation. At most 128 child requests may be outstanding and at most 65,536 distinct child IDs may be used in one generation.

2.1 notification#

Emit a user-visible notification.

json
{
  "jsonrpc": "2.0",
  "method": "notification",
  "params": {
    "level": "success",
    "title": "Hello",
    "message": "hello_world greeted tinkerer"
  }
}

level is one of: "info" (default), "success", "warning", "error".

This is a JSON-RPC notification (no id); the host does not reply.


2.2 confirmation/request#

Ask the user a yes/no question.

json
{
  "jsonrpc": "2.0",
  "id": "confirm-1",
  "method": "confirmation/request",
  "params": {
    "parent_request_id": 2,
    "prompt": "Push to origin/main?",
    "detail": "5 commits, +120 −30 lines",
    "destructive": false,
    "default": false
  }
}

The host answers the same id:

json
{
  "jsonrpc": "2.0",
  "id": "confirm-1",
  "result": {
    "confirmed": true
  }
}
  • The id must be a string no longer than 256 UTF-8 bytes, or an unsigned 64-bit integer.
  • parent_request_id is required and must name an active host request in API 0.2; frozen API 0.1 omits it.
  • The extension must wait for the answer before proceeding with the confirmed action.
  • Dropping the request or using a non-interactive frontend denies it.
  • Pending confirmation IDs carry a process generation; they cannot be answered against a replacement child after reload.

2.3 context/contribution#

Unsolicited prompt context pushed by the extension at any time.

json
{
  "jsonrpc": "2.0",
  "method": "context/contribution",
  "params": {
    "label": "file-watcher",
    "content": "src/main.rs was modified at 14:32.",
    "placement": "system_suffix"
  }
}

Notification (no id).


2.4 status/contribution#

Unsolicited semantic status contribution. It does not force any frontend to create persistent chrome; the coding TUI keeps extension state in explicit views.

json
{
  "jsonrpc": "2.0",
  "method": "status/contribution",
  "params": {
    "surface": "status",
    "text": "watching src/",
    "style_role": "extension.watcher.status",
    "priority": 10
  }
}

Notification (no id).


2.5 presentation/update (API 0.2)#

Requires contributes.presentation = true. Publish one complete semantic state snapshot as a JSON-RPC notification:

json
{
  "jsonrpc":"2.0",
  "method":"presentation/update",
  "params": {
    "parent_request_id":2,
    "snapshot": {
      "revision":4,
      "status":{"state":"active","label":"1 worker"},
    "activities":[{
      "id":"worker:1","kind":"delegation","state":"running",
      "summary":"Reviewing tests","provenance":"local child",
      "started_at_ms":1721000000000,
      "metrics":{"tool_calls":4,"input_tokens":12000,"cache_read_tokens":800,
        "cache_write_tokens":0,"output_tokens":220,"reasoning_tokens":60,
        "cost_microdollars":7200},
      "references":[]
    }],
    "collection":{
      "kind":"tree","title":"Workers",
      "nodes":[{
        "id":"worker:1","parent_id":null,"state":"running",
        "label":"test-review","secondary":"running",
        "action_ids":["stop"],"references":[]
      }],
      "selected_node_id":"worker:1",
      "detail":{
        "node_id":"worker:1","title":"test-review",
        "body":"Running in a bounded child session.","references":[]
      }
    },
    "actions":[{
      "id":"stop","label":"Stop worker","command":"workers",
      "arguments":["stop","worker:1"],"destructive":true
    }]
    }
  }
}

parent_request_id correlates a handler-time snapshot to the active host request; octet derives its owner rather than accepting a session name from the extension. A background publisher instead supplies the complete host-issued resource_owner triple it previously received; octet accepts it only if that exact triple was issued to this process generation. The fields are mutually exclusive. Omitting both declares process-scoped state, which must contain no session-owned data. Stale/foreign triples and snapshots for another active product owner are dropped.

revision is an unsigned monotonic process-generation revision (zero is valid); a newer process generation may restart it. status is optional. activities and actions default to empty arrays; collection is optional. Collection kind is list or tree. IDs are stable extension-scoped identifiers. Nodes use optional parent_id; lists cannot have parents, trees cannot contain a missing parent, cycle, or depth over 16. Activities may include optional metrics with tool_calls, disjoint input_tokens, cache_read_tokens, and cache_write_tokens, output_tokens, its reasoning_tokens subset, and optional cost_microdollars. Every counter uses the portable JSON integer bound and reasoning cannot exceed output. A detail node_id must match the current selected_node_id. Generic states are empty, loading, pending, active, running, succeeded, failed, cancelled, degraded, stopped, and unavailable.

References contain {kind,id,label?}. Kinds session, artifact, and resource carry opaque identifiers. A session reference is only a lookup key: frontends may open it read-only after the host separately verifies the issuing parent session, path-free extension principal, and resource owner; mutation continues through agent_sessions. Kind url carries a sanitized absolute HTTP(S) URL; credentials, localhost/.local, and private/loopback/link-local/ unspecified/multicast literal IP targets are rejected and frontends expose it only after a user click. Each action must route to an existing command declared by this manifest. Labels and detail are plain data: ANSI/control sequences, HTML, scripts, CSS, and frontend layout coordinates are rejected or rendered as text.

A snapshot is capped at 256 KiB encoded, 128 activities, 256 nodes, 64 actions, 16 tree levels, 8 references per item, 1,024 bytes per compact label/ID, and 64 KiB for detail body. Revisions and timestamps are capped at the largest exactly representable JSON integer. One generation emits at most 32 snapshots in a one-second window; excess valid notifications are coalesced last-wins and the newest complete snapshot is emitted when the next window opens. Throttling produces at most one diagnostic for that window. The host validates the complete snapshot atomically, attaches manifest identity, a non-repeating process-instance fence, generation, and active resource owner, ignores or diagnoses stale updates, and retains the latest accepted replacement for explicit TUI views, Serve, and bounded headless fallbacks. Generic snapshots do not become ambient chrome. The coding TUI recognizes owner-fenced octet-subagents activities as a first-party observed surface and updates one bounded tool-like Subagents transcript block in place from native AgentEvent::DelegationUpdated events, including between root turns. Its heading counts worker states and up to four active child lines show tasks and input/output tokens; the worker list retains the complete roster, metrics, and cost. The TUI does not poll a status command, and the extension cannot supply footer text or terminal rows. It clears stale state on owner/process replacement; Serve action identity includes the instance fence, generation, and revision before routing the selected manifest process's command. The notification never invokes an action, repeats work, mutates a tool result, or grants authority.


2.6 $/progress (API 0.2)#

Requires request_progress. Progress is correlated to one active host request and sequences must increase strictly for that request:

json
{
  "jsonrpc": "2.0",
  "method": "$/progress",
  "params": {
    "request_id": 2,
    "sequence": 7,
    "event": {
      "type": "status",
      "message": "Fetched 3 of 10 results",
      "current": 3,
      "total": 10,
      "unit": "results"
    }
  }
}

Event variants are:

  • status {message, current?, total?, unit?}
  • output {stream: "stdout"|"stderr", encoding: "utf8"|"base64", data}
  • decoration {label, detail?} additionally requires progress_decoration; bounded ephemeral annotation rules.

Inactive-request and non-monotonic progress is ignored with a diagnostic. Accepted output uses octet's existing 8 KiB chunking and bounded progress sink, which may coalesce/drop under pressure. Progress is ephemeral: it is not a model result and is not persisted in the conversation transcript.


2.7 input/request (API 0.2)#

Request ephemeral text from the frontend while an operation is active:

json
{
  "jsonrpc": "2.0",
  "id": "input-1",
  "method": "input/request",
  "params": {
    "parent_request_id": 2,
    "prompt": "Password:",
    "secret": true
  }
}

The host answers the same ID with { "value": string|null }. null means the frontend cancelled or could not answer. Prompts must contain non-whitespace text and are bounded to 16 KiB UTF-8; answers are bounded to 256 KiB UTF-8; the 1 MiB full-frame bound also applies. secret: true suppresses echo and ordinary editor handling in an interactive frontend. Secret answers stay on the private reply channel and are never placed in diagnostics, progress, session state, or persistence. Headless/unavailable input is cancelled rather than guessed. Parent settlement cancels the pending input request.


2.8 artifact/publish (API 0.2)#

Requires artifacts. Publish either small inline base64 data:

json
{
  "jsonrpc": "2.0",
  "id": "artifact-1",
  "method": "artifact/publish",
  "params": {
    "parent_request_id": 2,
    "mime_type": "image/png",
    "size": 1234,
    "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "data": {"encoding": "base64", "data": "..."}
  }
}

or a relative path under OCTET_EXTENSION_SCRATCH by replacing data with "path": "screenshots/result.png". Exactly one source is required. Success returns { "artifact_id": "..." }; malformed or rejected publication uses -32602, while a request without an active host-owned session context uses -32002.

The host securely reads a regular no-follow scratch file, snapshots immutable bytes, and verifies exact size, lowercase SHA-256, canonical MIME, and media signature. Defaults are 256 KiB inline, 20 MiB per artifact, 64 MiB and 64 artifacts per process generation, and a 4096-byte/64-component relative path. The 1 MiB JSON-line bound still applies. Supported media are PNG, JPEG, GIF, WebP, WAV, MPEG audio, FLAC, Opus, AAC, and MP4 audio. IDs are opaque and valid only for the host-derived session owner that published them and that process generation. Tool-result media resolution supplies the same owner internally; another owner sees an unknown artifact even if it learns the opaque ID. Reload, generation settlement, or an owner mismatch makes the handle unavailable.


2.9 policy/evaluate (API 0.2)#

Requires policy_intents:

json
{
  "jsonrpc": "2.0",
  "id": "policy-1",
  "method": "policy/evaluate",
  "params": {
    "parent_request_id": 2,
    "intent": {
      "kind": "external_side_effect",
      "operation": "browser.submit_form",
      "target": {"origin": "https://example.com", "label": "Publish comment"},
      "data_classes": ["user_text"],
      "adapter_hints": {"read_only": false, "destructive": false}
    }
  }
}

The response is { "decision": "allow"|"ask"|"deny", "approval_token"?: string }. The host owns classification. Adapter hints can only increase caution and do not authorize an action. confirmation/request remains cooperative UI, not policy enforcement.

The optional approvals feature adds a single-use retry boundary. If the host classifies an intent as ask and a trusted frontend approves it, the response remains ask but includes a 64-character lowercase hexadecimal approval_token. A token is valid only on ask; allow and deny responses must omit it. The extension must repeat policy/evaluate with the exact same intent, still-active parent_request_id, and that token:

json
{
  "jsonrpc": "2.0",
  "id": "policy-2",
  "method": "policy/evaluate",
  "params": {
    "parent_request_id": 2,
    "intent": {
      "kind": "external_side_effect",
      "operation": "browser.submit_form",
      "target": {"origin": "https://example.com", "label": "Publish comment"},
      "data_classes": ["user_text"],
      "adapter_hints": {"read_only": false, "destructive": false}
    },
    "approval_token": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  }
}

The host atomically consumes the token and returns allow only when it still matches the canonical original intent, active owner/parent, and process generation before its bounded expiry (at most five minutes). Expiry, reuse, or intent/parent/generation mismatch returns deny; a recognized mismatched token is consumed as well. Supplying a token without negotiated approvals is rejected with -32602. Approval capability state is invalidated on generation replacement. The coding product leaves approval-token issuance off. Its working-tree mcp.tool.call adapter permits the admitted octet-mcp process's exact active, owner-scoped tool call under unsafe_host, including mutations. It verifies the generation, exact published tool identity, and arguments against the host-issued call; commands, ownerless/settled parents, changed targets, and controlled policies cannot authorize it. Generic operations still return deny. This is not an annotation-based read-only exemption or an automatic replay grant.

An extension may send $/cancelRequest for one of its own outstanding child request IDs. The host also sends it automatically when the owning parent settles.


2.10 secret/get (API 0.2)#

Requires the conditionally offered secrets feature and an exact name from the manifest's [capabilities].secrets allowlist:

json
{
  "jsonrpc": "2.0",
  "id": "secret-1",
  "method": "secret/get",
  "params": {
    "parent_request_id": 2,
    "name": "browser.api_token"
  }
}

Success returns { "value": string }. Values are UTF-8, may be empty, and are capped at 64 KiB. octet derives the extension identity and the complete {session_id, extension_instance_id, process_generation} resource owner from the active parent rather than accepting either from child JSON. The broker receives that identity, owner, parent request ID, and exact logical name.

An undeclared/invalid name returns -32602; no active owner or service returns -32002. A broker returning no value or failing always returns the same -32004 secret is unavailable response, keeping provider details host-side. octet does not persist or log the value and best-effort wipes the host broker buffer and serialized writer frame after use. Once delivered, however, the extension holds an ordinary process-memory string: API 0.2 does not promise end-to-end zeroization. The coding product currently configures no broker and therefore does not offer secrets.


2.11 tools/register (API 0.2)#

Requires dynamic_tools. Add new tools or replace complete definitions for existing extension-owned names:

json
{
  "jsonrpc": "2.0",
  "id": "catalog-1",
  "method": "tools/register",
  "params": {
    "tools": [{
      "name": "ableton_tracks",
      "description": "List tracks in the active Ableton set",
      "parameters": {"type": "object", "properties": {}},
      "output_schema": null
    }]
  }
}

The request contains complete ToolDefinition objects, not patches. Names in one request must be unique. The request and complete prospective catalog are capped at 256 tools. octet validates the complete result, reserves names against host tools and other extensions, applies the active host tool policy, and publishes the extension group atomically. After a parseable request ID, malformed parameters, a schema error, duplicate, or name conflict return -32602 and leave the previously published group unchanged.

Success returns the new per-process epoch and the complete set octet accepted:

json
{
  "jsonrpc": "2.0",
  "id": "catalog-1",
  "result": {
    "revision": 1,
    "tools": ["ableton_tracks", "ableton_transport"]
  }
}

The returned list is authoritative: policy may omit requested tools. Revision 0 is the initialize catalog, and every accepted mutation increments it once. The epoch resets to 0 for a new process generation. If acknowledgement cannot be delivered after publication, octet removes the extension's dynamic group and terminates the generation so host and extension cannot continue with different catalogs.


2.12 tools/unregister (API 0.2)#

Requires dynamic_tools. Remove extension-owned names transactionally:

json
{
  "jsonrpc": "2.0",
  "id": "catalog-2",
  "method": "tools/unregister",
  "params": {"names": ["ableton_tracks"]}
}

Names must be unique valid tool identifiers; missing names are ignored. The same 256-name bound, revision rules, transactional publication, authoritative response shape, and acknowledgement-failure handling as tools/register apply. The Python SDK rejects an empty mutation locally; callers should not use no-op catalog changes as revision clocks.

The exact initialize catalog is authoritative epoch 0 and is the only deterministic first-request catalog. Once a post-initialize catalog change is accepted, octet advertises it at the next model-request boundary after publication. It does not infer catalog quiescence, so a registration sent immediately after initialize is not guaranteed to appear on turn one; put turn-one tools in the manifest and initialize response. A provider request already in flight retains the schema and tool implementation it was given. Calls therefore carry tool/call.catalog_revision; extensions must keep enough historical dispatch state for overlapping turns and reject an unknown or retired epoch with -32602. The Python SDK retains eight committed catalog snapshots and temporarily exposes its staged next epoch during the publication-before-ack window. A replacement generation likewise begins from its initialize catalog at epoch 0; its subsequent mutations obey the same next-boundary rule.


2.13 agent/spawn (API 0.2)#

Requires the conditionally offered agent_sessions feature and an active host model-tool or declared-command parent, unless the additive API 0.4 retained-owner contract below was negotiated. Create one bounded in-harness child model session:

json
{
  "jsonrpc": "2.0",
  "id": "agent-1",
  "method": "agent/spawn",
  "params": {
    "parent_request_id": 2,
    "task_name": "inspect-midi-tools",
    "profile": "review",
    "fingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "message": "Inspect the current Ableton MCP catalog.",
    "idempotency_key": "catalog-audit-2026-08-16",
    "policy": {
      "tools": ["read"],
      "max_depth": 1,
      "max_concurrent_children": 8,
      "max_turns": null,
      "max_tokens": null,
      "max_cost_microdollars": null,
      "max_output_bytes": 8192,
      "timeout_ms": null
    }
  }
}

The host derives the resource owner from parent_request_id; callers cannot invent an owner. API 0.4 agent_session_lifetime_v1 permits returning the original host-issued owner as described below. policy is mandatory. Its tools are a non-empty, duplicate-free subset of read, edit, write, and bash (the first-party extension defaults to all four; explicitly select read for hard read-only work; content search uses bash, which can mutate); depth is exactly one; concurrency is 1..=8; returned UTF-8 bytes are 512..=16,384. The turn, cost, and wall-time ceilings are optional per child: max_turns: null, max_cost_microdollars: null, and timeout_ms: null inherit the parent session's ceilings (an unlimited parent produces an unlimited child); explicit values are 1..=256 turns, 1..=50,000,000 microdollars, and 5,000..=86,400,000 milliseconds. max_tokens: null means exact inheritance of the parent's optional cumulative session-token setting, so a parent with no ceiling produces a child with no ceiling; a non-null 1,000..=64,000 value may request a stricter cap. Every child starts with a fresh context using the selected model's context window and an output limit capped by both the parent's resolved per-request limit and the selected model's output capacity. octet freezes a detached effective tool snapshot containing only the granted tools (no collaboration or agent tools), applies the requested ceilings or inherits the parent's ceilings when they are omitted, and owns limit settlement even when the extension is idle or restarted.

The idempotency key is 1..=256 bytes and scoped to the extension principal plus that owner. Retrying identical task_name/profile/fingerprint/message/ policy while the owning-run child record is retained returns the same result. Reuse with different input fails with -32002. At the next owning run, stale idempotency entries are pruned with their missing child records rather than returning a nonexistent session. task_name and optional profile are 1..=48 lowercase ASCII letters, digits, underscores, or hyphens. Optional fingerprint is one lowercase SHA-256 digest used only as opaque recovery metadata. The message is capped at 128 KiB. Success includes agent_id, agent_path, caller-visible task_name, optional profile/fingerprint, idempotency_key, status, effective policy, host-owned created_at_ms/started_at_ms/ completed_at_ms, and deadline_at_ms, plus the path-free extension principal and durable session resource_owner string.

Child model selection (agent_model_selection_v1)#

With the feature negotiated, policy.model_selection accepts {"provider":"inherit","model":"inherit","reasoning":"inherit"}. Omitting the object (or using null) preserves default inheritance; each omitted member defaults to "inherit". Explicit members are non-empty strings of at most 256 UTF-8 bytes without control characters; unknown members are rejected. An explicit provider requires an explicit model. Model/provider identifiers must resolve through the host's configured inventory, not an arbitrary URL, endpoint, or credential supplied by the extension. Supplying a non-null selection without negotiating the feature is rejected.

The host owns reasoning parsing, compatibility checks, normalization, and child lowering. Same-route inheritance preserves parent reasoning; a different route uses the host's supported translation or rejects incompatible reasoning rather than silently substituting a default. Discovery's reasoning labels are choices, not permission to bypass that policy. Astra V2 Ultra child reasoning is lowered to xhigh by the host. Unsupported routes or reasoning fail before child admission. Without a configured resolver, only the inherited binding is available.

Spawn and list records expose host-confirmed resolved_model, also included in effective policy.resolved_model, with provider, model, and reasoning. Unlike the requested reasoning string, resolved reasoning is a serialized ReasoningConfig object (for example {"type":"off"}). This secret-free metadata contains no credentials, headers, or transport URLs. The host pins resolved model/provider/reasoning identity for continuation and recovery; a saved selection that no longer resolves to that identity fails closed. This is not a promise to detect arbitrary transport configuration changes behind those IDs.

agent/models (API 0.2, agent_model_selection_v1)#

Requires both agent_sessions and agent_model_selection_v1. Request:

json
{"jsonrpc":"2.0","id":"models-1","method":"agent/models",
 "params":{"parent_request_id":2,"query":"sonnet","limit":50}}

The active host model-tool or declared-command parent supplies the resource owner; only negotiated API 0.4 retained calls may return an explicit issued owner. Discovery is root-owner-only and retains the same principal and process-generation fences as other agent/* requests. Missing, foreign, or inactive owners fail closed.

query is optional/null, a case-insensitive model/provider/display-name search of at most 128 UTF-8 bytes without control characters. limit is optional/null, defaults to 50, and must be 1..=100. Success returns {"models":[...],"truncated":false}; each row contains model, provider, nullable display_name, reasoning (supported string labels), context_window, and max_output_tokens. The host bounds discovery and marks excess matches with truncated; this is not an unbounded catalog dump. IDs are capped at 256 bytes, display names at 512 bytes, and reasoning at 32 labels of at most 256 bytes each. Rows are secret-free configured-route metadata, not transport or authentication configuration. Without a configured resolver the view contains only the inherited model (subject to the query).


2.14 agent/message (API 0.2)#

Send steering input to an owned child while preserving the child session:

json
{
  "jsonrpc": "2.0",
  "id": "agent-2",
  "method": "agent/message",
  "params": {
    "parent_request_id": 2,
    "target": "agent-1",
    "message": "Also check resource tools."
  }
}

Success returns { "delivered_to": string, "delivery": "steering"|"queued" }. The target may be the ID or path returned by agent/spawn, but must belong to this extension principal and resource owner's child-session trees. The message is capped at 128 KiB.


2.15 agent/follow_up (API 0.2)#

Queue a subsequent run on an owned child:

json
{
  "jsonrpc": "2.0",
  "id": "agent-3",
  "method": "agent/follow_up",
  "params": {
    "parent_request_id": 2,
    "target": "agent-1",
    "message": "Now summarize only callable tools."
  }
}

Success returns agent_id, agent_path, and delivery: follow_up when the child is active (the message is queued on the running session) and new_run when the child is settled (the child's durable session resumes as a new run). Ownership checks match agent/message; the follow-up is capped at 128 KiB. Follow-ups reject shut-down targets, targets with an interrupt in flight, and a full follow-up queue. A resumed run re-enters the host's turn/cost accounting; when the child's wall deadline has already elapsed the host re-anchors it from the child's requested timeout so the new run owns a fresh budget instead of starting an already-expired one, a still-future deadline is preserved across the resume, and a child without a timeout stays unlimited.


2.16 agent/list (API 0.2)#

Request { "parent_request_id": 2 }. Success returns the extension principal, derived resource_owner, persistence_error, and agents. Each agent record contains agent_id, agent_path, parent_id, public task_name, optional profile, durable idempotency_key/fingerprint, depth, opaque agent-session:* resource reference, tagged status, effective policy, host-owned created_at_ms/started_at_ms/completed_at_ms, deadline_at_ms, turn_count, host-observed tool_call_count, structured phase and optional tool_name, a bounded rolling recent_tools array of the last six tool calls (each with name, a flattened single-line args summary capped at 160 UTF-8 bytes, host started_at_ms, optional finished_at_ms, and an error flag) so owner-facing pickers can answer what a worker is doing without opening its transcript, cumulative disjoint usage, optional cost_microdollars, and principal/owner provenance. States are pending, running, completed (with host-byte-capped output), interrupted, timed_out, failed (with bounded error), and shutdown. Private delegation JSONL paths are never returned. A current owner-scoped presentation may route the opaque reference into Serve, /extensions inspect, or the native octet-subagents arrow-key worker list as a locked read-only transcript; the resolver separately verifies host-written parent-session, extension-principal, and resource-owner provenance. The TUI transcript panel starts at the live tail, supports bounded scrolling, and returns to the worker list on Escape or Left. All mutation continues through the owner-bound agent_sessions methods. The list contains only roots spawned by this principal/owner and their descendants. Child contexts remain independent and their tokens never become parent prompt context. Before the root run settles, octet stops and briefly joins these children, aggregates each child session's durable usage and exact cost (including picodollar remainder), and appends one delegated_agent usage record per child to the root session. The live TUI adds the current presentation cost only until those records are committed, so the cumulative footer never double-counts delegated spend; this ledger mirror is accounting rather than context sharing and is not charged to the parent's own-context token ceiling. Owner-scoped agent/list/agent/wait observation remains available after the owning root becomes inactive so the final terminal snapshot and transcript picker can settle; spawn/message/follow-up/interrupt mutation still requires an active owner and fails closed.


2.17 agent/wait (API 0.2)#

Request { "parent_request_id": 2, "timeout_ms": 30000 }. The timeout defaults to 30 seconds and is clamped to 1..=60,000 ms. The request returns immediately when no owned child is pending/running; otherwise it waits until all owned children settle or the deadline expires. Success is { "timed_out": bool, "snapshot": <agent/list result> }. Parent settlement cancels the wait.


2.18 agent/interrupt (API 0.2)#

Request { "parent_request_id": 2, "target": "agent-1" }. Success returns agent_id, agent_path, previous_status, and interrupt_requested. The host cancels the owned descendant tree when an active interrupt is requested.

All agent/* methods share the child-request bounds; child creation retains the eight-worker bound. After a parseable request ID, malformed parameters return -32602. Unavailable service/owner, invalid ownership, exhausted delegation limits, persistence failure, or an invalid operation return -32002. The extension's stable principal is derived from its manifest name plus a SHA-256 manifest-identity digest; the manifest path itself is never returned to the extension. A different extension or resource owner cannot list, message, follow up, wait on, or interrupt its child trees. Ownership uses that principal plus the durable session-owner string, not the extension process generation, so supervised restart/reload can resume an existing tree. Process shutdown requests shutdown of the service's owned trees; a complete process-host rebuild creates a new service boundary. Hosted agents are a separate capability; these methods create octet child conversations. Observe their state through agent/list/agent/wait. Delegated child turns do not currently emit extension session/* or turn/* lifecycle notifications; that notification stream covers the owning/root product session.

API 0.4 child observations and retained lifetime#

agent_session_lifetime_v1 allows agent/* requests to include the original host-issued resource_owner alongside the required parent_request_id. Its session, extension-instance, and process-generation tuple must still be issued on this connection. With a live parent, the owner must match and normal parent cancellation/settlement applies. With no active parent, a still-issued session owner permits a new call; a known cancellation tombstone refuses that parent reference. This is session-scoped authority: cancelling one request does not revoke the owner's ability to make new calls. Retire the owner to revoke it. Without an explicit owner, the existing active-parent contract is unchanged. Owner retirement invalidates further calls, wakes in-flight waits and requests shutdown of only that owner's child trees. This is not an editor-checkpoint exception, a new grant, or permission to bypass native delegation limits.

With agent_session_events_v1, agent/events accepts:

json
{"parent_request_id":2,"target":"agent-1","after_sequence":0,"timeout_ms":25000}

after_sequence is a required nonnegative integer; timeout_ms defaults to zero and is bounded to 25,000 ms (not silently clamped). Success returns agent_id, nullable native session_id, events ({sequence,event} records), next_sequence, has_more, and tagged native status. Cursors are non-consuming and ordered, not transcript offsets. Future or expired cursors fail rather than fabricate missing observations. Each batch contains at most 256 records and 512 KiB of event bodies; the retained log is bounded to 4,096 records / 2 MiB. Event bodies over 256 KiB become explicit observation_error records. Native facts include model-turn start/finish, output delta/discard, tool start/finish and run finish, with timestamps. Unsupported media, compaction-mirror replacement and uncertain usage are explicit observation errors, not a claim of full Pi message projection.

With agent_session_lifetime_v1, agent/stop accepts {"parent_request_id":2,"target":"agent-1"} and returns {"agent_id":"agent-1","shutdown_requested":true}. It requests shutdown of that owned tree, not unrelated siblings. The acknowledgement does not mean settlement, cleanup completion or rollback; observe native state separately. Both methods enforce the same principal/owner target isolation and request bounds as the existing child service. They are unavailable in API 0.2/0.3.

2.19 composer/get, composer/set, composer/insert (API 0.2, feature composer)#

Host-owned composer access. composer/get requests { "parent_request_id": 2 } and returns { "text": "<bounded composer text>" }. composer/set requests { "parent_request_id": 2, "text": "..." } and replaces the whole composer; composer/insert requests the same shape and inserts the text at the host composer cursor. Both ordinary mutations return {}. While a remote custom editor owns the composer, plain mutations are refused; API 0.4 negotiated remote_ui permits the owner/mount/revision-fenced composer/set editor checkpoint instead, with an explicit revision ACK.

json
{ "jsonrpc": "2.0", "id": "pi:4", "method": "composer/set",
  "params": { "parent_request_id": 7, "text": "hello" } }

The host resolves the foreground resource owner from parent_request_id; any other owner is refused with -32002 not_foreground_owner. Composer text is bounded UTF-8 without lone surrogates or terminal control characters, at the same 256 KiB cap as the editor handoff. A local refusal is reported to the extension with the bounds_exceeded or invalid_request token before anything is written, so an oversized composer mutation never reaches the host. ctx.ui.editor is a separate, declared reduction: it seeds the requested prefill and focuses the host editor through the negotiated editor_handoff surface, the edited text stays host-owned, and the bridge never fabricates or returns one.

2.20 shortcut/register (API 0.2, feature shortcuts)#

Request { "parent_request_id": 2, "id": "pi:0:ctrl+shift+p", "key": "ctrl+shift+p", "description": "..." }. Success returns {}. Registration is written inside the initialize request scope, so parent_request_id is the initialize request and the id is stable for the process generation. At most 64 runtime shortcuts are admitted per process; the id is at most 128 UTF-8 bytes, the key at most 128 bytes, and the description at most 4 KiB. A shortcut without a handler, a duplicate id, a refused registration, or a registration arriving after the initialize request settled leaves the shortcut unavailable; a shortcut/trigger for it fails closed.

2.21 session/append_entry, session/set_name, session/set_label (API 0.2, feature session_entries)#

session/append_entry requests { "parent_request_id": 2, "entry_type": "custom", "data": <bounded JSON> } and returns { "entry_id": "..." }, the host-assigned durable entry identifier. session/set_name requests { "parent_request_id": 2, "name": "..." }; an empty name clears it. session/set_label requests { "parent_request_id": 2, "entry_id": "...", "label": "..." }; an empty label clears it. Both mutations return {}.

entry_type is bounded to 128 UTF-8 bytes, the opaque data payload to 64 KiB of bounded plain JSON (no functions, exotic prototypes, non-finite numbers, or nesting beyond the protocol limit), the entry id to 256 bytes, and the name and label to 4 KiB. Every method is owner-scoped through parent_request_id and is refused with -32002 not_foreground_owner for any other owner.

2.21b Active-session control (API 0.4, feature session_control_v1)#

This optional service targets the product's active session, not extension-owned agent/* children. It is offered only with a real bound lifecycle service and executes through its existing idle-boundary driver. Canonical API 0.3 keeps its separate session_lifecycle capability and request shapes unchanged.

Every request has closed params with required parent_request_id: u64 and optional/null resource_owner (the exact previously issued triple). The only other accepted field is session_id, required for switch and absent/null for all other methods:

Method session_id Successful result / effect
session/wait_for_idle absent/null {"session_id":"..."} for the active durable session, only after an observed idle boundary; no mutation
session/create absent/null {"session_id":"..."} for a newly durable session; does not switch
session/fork absent/null {"session_id":"..."} for a durable fork at the active head; does not switch
session/switch existing workspace session ID, at most 256 UTF-8 bytes {"session_id":"..."} after making that session active; not an arbitrary file path
session/reload absent/null {"session_id":"..."} after reopening the active durable session
json
{"jsonrpc":"2.0","id":"idle-1","method":"session/wait_for_idle","params":{"parent_request_id":7,"resource_owner":{"session_id":"host-issued-owner","extension_instance_id":"host-issued-instance","process_generation":1}}}

A live parent's owner takes precedence; the request remains its ordinary child and parent settlement/cancellation cancels it. An explicit owner does not detach a call made while the parent is live. With a settled parent, a still-issued owner permits an independently answered request; missing, foreign or stale owners and known cancellation tombstones are refused. Owner validation is not an admission-success acknowledgement or replay permit.

The bounded queue preserves operation order. Idle waits cannot overtake earlier mutations, and no method returns success before its actual consumer completes. An active handler must not wait for a mutation whose idle boundary depends on that handler settling. Deactivation/activation epochs fence queued work from a replacement application. Cancellation does not roll back a committed mutation. Admission uses typed unsupported_feature, invalid_request and not_foreground_owner refusals; lifecycle queue/worker exhaustion returns -32012, and unavailable/failed lifecycle execution returns -32603.

2.21c session/compact (API 0.4, feature session_compaction_v1)#

Source status: newly authored Rust dispatch, consumer and tests are uncompiled and unqualified. The source binding is the isolated interactive lifecycle consumer, not a claim of installed, headless or Serve support.

Negotiation requires both session_compaction_v1 and session_control_v1. The host offers compaction only when a real lifecycle receiver has opted in via ExtensionSessionLifecycleService::with_compaction() before process startup; ordinary channel() alone does not enable it. Dispatch additionally requires the actual opted-in service and active frontend binding. API 0.1/0.2 and canonical 0.3 cannot negotiate this service.

The request envelope and params are closed. Required fields are parent_request_id: u64 and resource_owner, the complete exact host-issued {session_id,extension_instance_id,process_generation} triple. custom_instructions is optional string/null, capped at 16 KiB raw UTF-8 before normalization. LF and TAB are allowed; other controls are refused. Blank instructions are valid; the native Agent trims and normalizes them to absent.

json
{"jsonrpc":"2.0","id":"compact-1","method":"session/compact","params":{"parent_request_id":7,"resource_owner":{"session_id":"host-issued-owner","extension_instance_id":"host-issued-instance","process_generation":1},"custom_instructions":"Keep the open questions."}}

Send only after the originating host request's successful terminal reply has been written. A live parent is deterministically refused with -32602 invalid_request, including recursive awaited calls from a compaction hook. Do not await idle or compaction while settling that handler. This is a distinct post-reply service, not a blanket editor-checkpoint or ordinary-child lifetime exception. Retained contexts may send once no handler scope is live, using their original issued owner and cancellation scope.

For an adapter exposing synchronous void ctx.compact(), enqueue locally in the handler scope and flush the reverse request only after serially writing the successful host reply. On parent error/cancellation suppress the request and invoke onError; never include compaction in the handler's awaited flush. Refuse calls from session_before_compact/session_compact, including deferred callbacks scheduled by those hooks, rather than starting a recursive callback chain.

The issued owner is session-scoped authority after settlement, not a success ACK or replay token. A known parent-cancellation tombstone refuses the request; cancelling one request does not itself revoke every future call by that owner. Owner retirement, process close/drain or replacement generation, child cancellation and lifecycle-epoch change fence queued and executing work. The consumer also checks the actual foreground Session owner. Native cancellation is cooperative: the operation and provider accounting are driven to settlement, not abandoned when the caller disappears.

There is no queue-admission success. Exactly one terminal outcome is admitted; success follows actual native Agent compaction, durable commit and its awaited after-hooks:

json
{"jsonrpc":"2.0","id":"compact-1","result":{"entry_id":"checkpoint-id","summary":"Actual stored summary.","first_kept":"retained-entry-id"}}

These three strings come from the newly appended native Compaction entry, not session.head (an after-hook may append private metadata). No token counts, usage, details or other metrics are fabricated. Unsafe/no compactable history, a before-hook veto, unavailable consumer or Native Responses mode is a refusal, not successful compaction; this is the cancellable local service.

Admission errors use unsupported_feature (-32601), invalid_request or bounds_exceeded (-32602), and not_foreground_owner (-32002). Execution failures use JSON-RPC errors (-32603), not success-shaped values. A post-commit after-hook failure explicitly retains the checkpoint and forbids retry; it cannot roll back or replay compaction. Oversized/undeliverable results are not truncated into success. Lost delivery or cancellation never authorizes automatic replay of possibly committed work.

2.22 session/send_message, session/send_user_message (API 0.2, feature message_injection)#

session/send_message requests { "parent_request_id": 2, "role": "assistant" | "system", "text": "..." } and session/send_user_message requests { "parent_request_id": 2, "text": "..." }; both return {} after the host admits the bounded injection. Any other role, including user, is refused locally with invalid_request rather than coerced. Injected text is bounded at 256 KiB and both methods are owner-scoped through parent_request_id.

2.23 tools/set_active (API 0.2, feature active_tools)#

Request { "parent_request_id": 2, "names": ["tool", ...] }; success returns {}. names is the complete replacement active tool set, bounded like tools/register (at most 256 names, each at most 128 UTF-8 bytes). A host build that cannot apply dynamic tool policy refuses the request instead of accepting an unapplied set. The method is owner-scoped through parent_request_id.


2.23b pi.sendMessage mapping (bridge surface)#

pi.sendMessage has three shapes in the installed corpus, and the bridge maps each one onto a real host op instead of a single coercion:

call octet path notes
pi.sendMessage({ role: "user", ... }) session/send_user_message the ordinary user-turn path
pi.sendMessage({ customType, content, display, details }, { triggerTurn }) session/append_entry (entry type = customType) plus session/send_user_message when a turn is requested the dominant corpus form (13 of 28 installed packages); needs both session_entries and, for the turn, message_injection
pi.sendMessage({ role: "assistant" | "system", ... }) refused: unsupported_feature octet never fabricates a provider turn, so an extension-authored assistant/system message is refused rather than coerced

The custom-message form's display: true is recorded in the entry payload but not yet honored: the shell does not project extension entries into the transcript, so rendering a custom message is the entry-renderer tier of P9. This is a declared divergence, not a silent drop.

2.24 terminal/acquire, terminal/release (API 0.2, feature terminal_handoff)#

Foreground terminal handoff. terminal/acquire requests { "parent_request_id": 2 } and returns { "grant_id": "<bounded id>", "columns": <u16>, "rows": <u16> } once the frontend that owns the foreground session's tty has actually ceded it. terminal/release requests the same shape and returns {} once the frontend has re-entered its own terminal and input loop.

json
{ "jsonrpc": "2.0", "id": "pi:9", "method": "terminal/acquire",
  "params": { "parent_request_id": 7 } }

The terminal stays host-owned. The grant is exclusive, owner-scoped, and revocable at any moment: the host refuses acquire while another grant is live (invalid_request), refuses release from a caller that does not hold the current grant (invalid_request), and refuses both when no foreground terminal is available (not_foreground_owner/invalid_request). grant_id is minted by the frontend and bounded to 128 UTF-8 bytes; the extension never supplies it. The host may take the grant back without being asked — the holder crashed, the session or process generation moved, or a coordinated shutdown force-restored the terminal — in which case the holder receives terminal/grant-lost with a bounded reason and must stop writing to the tty. A holder that keeps writing after a revocation is writing to a terminal the host has already restored.

While a grant is live the host emits no ui/terminal-input or ui/resize observation to the holder (it owns the tty for the duration), and the same process must not be handed a second grant. The dispatch-level refusal is typed: unsupported_feature when terminal_handoff was not negotiated, and invalid_request/bounds_exceeded/not_foreground_owner for everything else.

2.24b Host-mediated autocomplete (feature autocomplete)#

The base profile below retains its existing behavior. API 0.4 may negotiate autocomplete_edit_v1 for the additive edit fields and prefix/value control exceptions described below.

ui/autocomplete/register accepts { "revision": <u64> }. Registration is admitted by an interactive frontend and acknowledged with { "accepted": true }; an unavailable frontend refuses rather than fabricating an active chain. For an admitted chain, the host sends ui/autocomplete/complete with { "text": "...", "cursor": <UTF-8 byte offset>, "revision": <u64> }. The cursor must be a character boundary in the bounded editor snapshot. Reply with { "prefix": "...", "items": [{ "value": "...", "label": "...", "description": null }] }. Prefix is the exact suffix before the cursor to replace; text fields are at most 1,024 UTF-8 bytes with no controls. At most 32 items may be returned.

An empty list leaves the query unclaimed. The coding frontend tries the next registered live chain and then ordinary native path completion, under one 500 ms aggregate deadline. Both menus and fallback apply only if the saved text, cursor, revision and focused-editor state still match; a late empty reply cannot edit a new draft. Extension choices use suffix replacement, not arbitrary editor edits. This contract alone does not cover every Pi completion application's quote, cursor, multiline or wrapper semantics.

API 0.4 profile: autocomplete_edit_v1#

This optional feature is offered only on API 0.4 and requires negotiated autocomplete; selecting it without autocomplete rejects initialization. API 0.1, 0.2 and canonical 0.3 cannot select it. Registration and the ui/autocomplete/complete request/response RPC stay unchanged; this profile adds two optional fields to each item:

Item field Type Absent default Validation
replace_after_bytes u32 0 At most 262,144 additional bytes of the original snapshot after the cursor; replacement end must be in range and on a UTF-8 boundary
cursor_offset_bytes u32 UTF-8 byte length of value Offset within inserted value, from zero through its byte length, on a UTF-8 boundary

These are nonnegative integer fields, not nullable fields. Explicit null, negative/fractional/out-of-u32 values and unknown fields are invalid. Without negotiated autocomplete_edit_v1, either field's presence is invalid even when its value is zero. Absence retains the base insertion/cursor defaults.

With this profile, prefix and item value permit LF, TAB and CR, but no other control characters or terminal escapes. Labels and non-null descriptions remain plain/control-free. The existing 1,024 UTF-8 byte bound applies to each string, and the response still has at most 32 items. The request snapshot and resulting editor text remain within the existing 262,144-byte editor budget; the wire frame budget is unchanged.

For original snapshot text and byte cursor c, let start = c - prefix.len() and end = c + replace_after_bytes (using the absent default). The host requires prefix to be the exact suffix of text[..c], checks original cursor/start/ end UTF-8 boundaries and range, then replaces text[start..end] with value. The resulting cursor is start + cursor_offset_bytes, measured within the inserted value, not within the original text. The inserted offset must be a UTF-8 boundary, and the complete resulting text must fit the editor budget. The entire response is validated against the exact original request on receipt; range, UTF-8 and budget validation is repeated at display and explicit acceptance.

Example after both features have been negotiated (cursor immediately before an existing quote):

json
{"jsonrpc":"2.0","id":42,"method":"ui/autocomplete/complete","params":{"text":"é\"","cursor":2,"revision":7}}
json
{"jsonrpc":"2.0","id":42,"result":{"prefix":"é","items":[{"value":"文/\"","label":"Directory","description":"Keep the closing quote","replace_after_bytes":1,"cursor_offset_bytes":4}]}}

Acceptance replaces the two-byte é plus the following one-byte quote with 文/" (five UTF-8 bytes), placing the cursor at byte four, after / and before the retained closing quote. Displaying a choice alone never performs that edit.

The coding host keeps correlation host-side, not in extension-supplied DTOs: the durable session/resource owner and live registered provider instance and process generation are rechecked at display and acceptance. Retired/reloaded/ replaced providers, changed sessions, changed text/cursor/revision or lost normal-editor focus invalidate results and displayed menus. A remote custom editor owning the composer also refuses normal-editor completion. Unclaimed responses keep the same next-chain/native-path fallback and aggregate deadline; fallback retains the queried provider chain's live fences and exact editor snapshot, so a late empty result cannot mutate a new draft.

The native editor additionally requires grapheme boundaries. It previews edits through checked editor APIs and refuses an unrepresentable range or cursor, rather than silently flooring/moving the requested byte cursor or bypassing editor invariants. Valid UTF-8 boundaries alone therefore do not guarantee native acceptance. This native profile does not establish Pi adapter support, full Pi completion parity or completed lifecycle qualification.

2.25 composition/context, composition/call, composition/store (API 0.4, feature tool_composition_v1)#

This is an additive feature-negotiated API 0.4 service. API 0.1, 0.2 and canonical 0.3 cannot select it. A tool definition may declare:

json
{
  "name": "compose",
  "description": "Run a bounded program",
  "parameters": {"type":"object","properties":{"code":{"type":"string"}},"required":["code"],"additionalProperties":false},
  "composition": {"mode":"on","inline_budget":3000}
}

mode is on (ordinary direct tools plus composition) or only (composition advertised, ordinary tools nested-only). inline_budget is an integer from 0 through 16000 estimated tokens. Unknown composition fields are rejected. Optional constrained_sampling remains the existing provider grammar/regex contract. A composition declaration without negotiated tool_composition_v1 is invalid. Presentation never expands the policy-filtered registry or enables excluded tools; nested composition tools are removed to prevent recursion.

Every request below has an exact JSON-RPC request envelope and includes the numeric parent_request_id of a currently active model tool/call in this extension process generation. No caller-supplied resource owner is accepted. Initialization, commands, hooks, unbound, stale, foreign and settled parents are refused. Parent settlement/cancellation revokes executing and queued children; cancellation of a reverse request also cancels its nested operation. Late replies cannot revive authority. Ordinary extension effects and controlled profile restrictions still apply to the outer call.

Frozen context:

json
{"jsonrpc":"2.0","id":"context-1","method":"composition/context","params":{"parent_request_id":2}}
json
{"jsonrpc":"2.0","id":"context-1","result":{"tools":[{"name":"read","description":"Read a file","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]},"output_schema":{"type":"object"}}],"store":{},"limits":{"timeout_ms":30000,"max_calls":256}}}

The tools, schemas, workspace, effect broker, hooks and cancellation belong to the exact frozen outer-call snapshot. No model routes, credentials or implicit LLM authority are provided. The store is private branch-ancestry state scoped by the composing tool's host-derived name.

One nested call:

json
{"jsonrpc":"2.0","id":"call-1","method":"composition/call","params":{"parent_request_id":2,"name":"read","arguments":{"path":"README.md"}}}
json
{"jsonrpc":"2.0","id":"call-1","result":{"value":{"content":"Example","path":"README.md","hash":"...","start_line":1,"end_line":1,"total_lines":1,"next_offset":null,"truncated":false,"lines_clipped":false}}}

name is an exact frozen tool name (1..128 UTF-8 bytes); arguments is an object bounded to 128 KiB encoded JSON and depth 32. The normal host argument validator runs before hooks or effects. Each invocation receives a host-issued nested ID and fresh effect reservation; safe reads may overlap at most four at a time, while mutations, shell and extension effects are exclusive. There are at most 256 calls and one 30-second host deadline per parent, not per child. Guest preferences can only lower limits.

The host returns programmatic/structured content for a schema-declaring tool, or text for a schema-less tool. It never parses arbitrary text as JSON or implicitly publishes nested media/raw stdout to chat. Extension output-schema validation remains in force. Nested errors use the JSON-RPC error envelope; policy refusals, unavailable tools and revoked authority do not become values. Completed nested usage is collected even if the program later fails or is cancelled, durably accounted before the next admission, and left unpriced when no authoritative model route/pricing exists. Hard token/cost ceilings refuse nested tools lacking host-authoritative unmetered/pre-execution bounds.

Commit successful store writes:

json
{"jsonrpc":"2.0","id":"store-1","method":"composition/store","params":{"parent_request_id":2,"set":{"answer":42},"delete":["obsolete"]}}
json
{"jsonrpc":"2.0","id":"store-1","result":{}}

The adapter submits writes only after successful noncancelled guest execution. The host accepts at most one commit per parent, durably syncing private metadata before acknowledgment. There are at most 4096 keys per write batch, keys at most 1024 UTF-8 bytes, each value at most 256 KiB JSON and the entire resulting store at most 1 MiB. Unknown fields/invalid JSON are refused. Failed/cancelled scripts do not submit writes. Earlier completed effects are not undone. Reopen, fork, switch and compaction use durable active ancestry; portable conversation exports omit this private sidecar. Started-but-unfinished nested effects and outer scripts are not automatically replayed.

Oversized private JSON transport: the normal wire frame bound stays 1 MiB. A context or value too large for a frame uses exactly one sidecar envelope:

json
{"jsonrpc":"2.0","id":"context-1","result":{"context_file":{"path":"composition-random.json","bytes":1500000,"sha256":"64-lowercase-hex-digits"}}}
json
{"jsonrpc":"2.0","id":"call-1","result":{"value_file":{"path":"composition-random.json","bytes":1500000,"sha256":"64-lowercase-hex-digits"}}}

path is a host-created flat basename beneath OCTET_EXTENSION_SCRATCH, never an absolute path or guest-selected path. Files use exclusive private creation, are regular/no-follow, have exact positive size at most 8 MiB and a SHA256 digest. Readers check size, identity, digest, UTF-8 and JSON before use, then unlink in finally. The host tracks at most 256 such files per parent and cleans undelivered/cancelled/settled files. No filesystem capability is exposed to guest code. A value above the limit produces an actionable error, not a widened frame or fallback to raw output.

Private receipts contain argument digests and host policy/outcomes, not normal raw nested output. A tool with provisional delivery can acknowledge only after an exact private text receipt is synced, bounded by the smaller of the sandbox output cap and 1 MiB; oversized or failed persistence rolls it back.

Bounded complete outcomes (API 0.4): composition/call may add full_outcome:true, returning the native host-issued tool_call, canonical ordered content, is_error, and present metadata/structured-content/usage. Unknown tools, schema failures, hooks and policy failures become host-created error outcomes; transport revocation remains an RPC refusal. Raw JSON arguments are admitted only for complete outcomes, before preparation and schema checks; ordinary composition still requires object arguments. Absent structured content is not converted to null. Complete outcomes use the existing bounded value sidecar.

updates:true additionally requests live composition/update notifications, with {request_id,sequence,result} bound to that exact reverse call. Only native partial-result snapshots are relayed. The channel holds at most 64 snapshots, is nonblocking, and may drop progress under pressure; result/effect settlement is not stalled by slow callbacks. Parent/call cancellation and settlement revoke updates. Updates never enter the transcript or model context, and remain subject to the existing 1 MiB frame bound (no progress sidecar).

During active tool/call, negotiated request_progress + content_parts admit $/progress event {type:"partial_result",result:{content,is_error?,metadata?,structured_content?}}. Partial snapshots reuse final-result content-part limits, private detail budgets, and exact generation/session artifact resolution including MIME/aggregate-media checks, but do not need to satisfy the final output schema. Inactive or non-monotonic updates are ignored. This addition is not a Pi parity qualification.

2.26 Native resource lifecycle (API 0.4)#

The optional resource_refs_v1 and operation_descriptors_v1 features add extension-local native values to existing tool calls. They do not create a remote-method interface. The closed ResourceRef value is {"$resource":"<opaque host token>","type":"Circuit"}. Type IDs are nominal, 1–128 ASCII bytes (letter first, then letters/digits/underscore/dot/hyphen); tokens are at most 128 ASCII bytes. Native objects never leave the extension.

ToolDefinition's optional operation is a closed descriptor with id, optional receiver, resource_inputs and resource_outputs. Inputs contain {path,type,access:"exclusive"}; outputs contain {path,type}. Paths are fixed JSON Pointers through schema object properties, not arrays or wildcards. Schemas must declare the exact closed ResourceRef shape and matching nominal type using const or a singleton enum. The receiver, when present, names an input path; it is presentation metadata, never argument injection or authority. Resource-bearing descriptors require both features. Catalog publication validates schema, descriptor and frozen handler together, including dynamic mutations.

Direction/method Closed params Result
Extension → host resource/register {parent_request_id,type} Provisional ResourceRef
Extension → host resource/release {parent_request_id,resource} {retired:true,cleanup:"pending"} (or retained completed/failed/unknown cleanup status)
Host → extension resource/dispose {resources:[ResourceRef,...],reason:"retired"} {results:[{resource:ResourceRef,status:"completed"},...]}; report failed for disposer failure

Reverse requests require a live, host-owned tool/call parent and never accept caller-supplied session authority. Registration alone grants no access. Complete successful parent admission atomically activates declared exports; failure, invalid output or cancellation winning first retires all provisional values. All input resources are checked and exclusively pinned at actual dispatch, including queued calls and non-receiver slots. Cancellation does not release pins before execution settlement or generation death. Release rejects pinned values with resource_busy; successful invalidation precedes separate cleanup. Unknown cleanup triggers bounded generation termination, not resurrection.

Initialization advertises protocol.limits.resource_refs_v1 with max_records (256) and max_registrations_per_parent (32); the current implementation uses these fixed bounds. Owner changes, accepted reload and host/process restart invalidate resources. A failed candidate reload leaves the old generation live. See the normative resource/descriptor contract.

Host lookup is opt-in through ExtensionHost::enable_operation_discovery() and its get_applicable_operations tool, not another extension RPC. It validates the resource first, filters exact nominal matches by live policy, then orders by operation ID/input path. Default limit is 8 cards, maximum 32, with fenced pagination. Only selected exact schemas enter the next model request; in-flight handlers stay frozen and live policy still applies. Ordinary/Pi registry and active-tool semantics are unchanged.

2.27 Immutable bulk (API 0.4, feature bulk_objects_v1)#

This optional feature is offered only when the host configures BulkStorage. Initialization adds protocol.bulk_objects_v1 with profile:"local-file.v1", absolute host-owned transfer_directory and finite limits. This context is transport-only, never model/domain data. A BlobRef is a closed value:

json
{"$blob":"<opaque host token>","bytes":1024,"digest":{"algorithm":"sha256","value":"0000000000000000000000000000000000000000000000000000000000000000"},"media_type":"application/octet-stream"}

The example illustrates shape, not an issued grant. Identity is $blob, never the digest. Bytes are portable nonnegative integers; digests require exactly 64 lowercase hex digits. Metadata must match the host record. SDK-generated schemas use the closed sha256 descriptor; domain wrappers such as WaveformRef remain ordinary typed records, not kernel numerical formats.

All four extension → host methods require a live parent_request_id belonging to an admitted tool call. Request objects are closed; no caller owner/path is accepted:

Method Params in addition to parent_request_id Result
bulk/write {profile:"local-file.v1",capacity,media_type} {ticket,profile,locator,capacity}
bulk/commit {ticket,bytes,digest} Provisional BlobRef
bulk/read {profile:"local-file.v1",blob:BlobRef} {lease,profile,locator,bytes}
bulk/release {id} (ticket or lease) {released:true}

A locator is a flat host-created basename in the negotiated transfer directory, valid only with its ticket/lease. Resolve without following symlinks; traversal, absolute paths and foreign grants are refused. The host copies and verifies a private immutable snapshot, so later producer writes cannot alter publication. Commit consumes the ticket; it does not yet export the blob. Complete successful parent admission publishes resource/blob outputs together; invalid output, cancellation or failed publication leaves no exported record. Diagnostic attachments alone cannot export provisional blobs. Close files/mappings before releasing leases. Known IDs or equal digests never imply authorization.

Default limits are object_bytes:268435456, owner_bytes:536870912, write_tickets_per_generation:8, read_leases_per_generation:32, and blobs_per_owner:256. Owner byte/record bounds apply per host session, including reservations. Hosts may explicitly configure different finite bulk limits; existing media-artifact limits are not raised. These are protocol quotas, not OS filesystem confinement of a trusted subprocess.

Retained blobs survive producer restart; tickets/leases and native resources do not. Durable host restart recovery requires the owning host's explicit BulkStorage::retain_durable/recover_durable path, verified bytes and fresh session authorization. A temporary store refuses durability. No durable-retain RPC or automatic native-object restoration exists. Results/model requests carry only BlobRef descriptors and bounded summaries, never bytes or locators. See the bulk lifecycle contract.

Typed diagnostics in tool results#

metadata.octet_diagnostics_v1 holds closed Diagnostic records with required severity, code, message and optional primary, related, fixes, attachments. The profile has at most 32 diagnostics and 64 KiB total. SDKs validate it and emit a bounded explicit text summary; the host validates shape and independently checks blob/artifact authority. Fixes are suggestions, not permission to edit. Failed operations cannot publish new references through diagnostics. The shared SDK contract specifies locations, UTF-8 byte spans, revisions, control handling and individual bounds. These additions and their conformance matrix do not imply complete Pi compatibility or completed qualification.

3. Standard JSON-RPC errors#

Code Message Meaning
-32700 Parse error Invalid JSON was received
-32600 Invalid Request JSON is not a valid Request object
-32601 Method not found Method does not exist / is not implemented
-32602 Invalid params Method arguments are invalid
-32603 Internal error Internal JSON-RPC error
-32800 Request cancelled API 0.2 cooperative cancellation won
-32000 to -32099 Server error Reserved for implementation-defined errors

Extensions should use -32601 for unknown methods and -32602 for invalid parameters. Custom errors in the -32000 to -32099 range are reserved for extension-specific server errors.

Wave-1 contract error names#

The Wave-1 composer, shortcut, session-entry, message-injection, and active-tool requests share one refusal vocabulary. A host refusal carries the numeric code and the contract name as the first token of error.message:

Code Contract name Meaning
-32601 unsupported_feature The method is not negotiated by this host build
-32602 invalid_request The params are malformed or outside the admitted shape
-32602 bounds_exceeded A string, payload, or batch exceeds its byte/element cap
-32002 not_foreground_owner The resource owner of parent_request_id is not the foreground owner

Refusals stay typed at the Pi surface: the extension sees an error whose first token is the wire contract it called followed by the contract name, so a void Pi setter can never hide a refusal. API 0.3 providers are unaffected and do not negotiate these names.


4. Type reference#

ExtensionIdentity#

Field Type Description
name string Stable extension name (matches manifest)
version string Semantic version
manifest_path string Absolute path to extension.toml
source string "project", "global", or "explicit"

ExtensionHostState#

Field Type Description
session_id string | null Stable session identifier
session_name string | null User-assigned session name
model string | null Canonical current model ID
reasoning value | null Reasoning configuration
active_skills array Explicitly active skills

ActiveSkill#

Field Type Description
id string Stable skill identifier
name string Human-readable skill name
version string | null Skill version

ExtensionExecutionContext#

Field Type Description
workspace string Active workspace root
execution_scope string | null Tool execution scope ID
resource_owner object | null API 0.2 host-derived {session_id, extension_instance_id, process_generation} on every session-owned tool, hook, command, context, status, or renderer boundary; omitted only for process-scoped/unsolicited contributions and in 0.1
host object Current ExtensionHostState

ExtensionResourceOwner (API 0.2)#

Field Type Description
session_id string SHA-256-derived durable canonical-session-path namespace; never supplied by model arguments
extension_instance_id string Host-created instance fence that changes across a complete process-host rebuild, including when generation numbering restarts
process_generation unsigned integer Reload/automatic-restart fence within one host instance for rejecting stale extension handles

ToolDefinition#

Field Type Description
name string Tool name; initialize names match the manifest, while negotiated dynamic_tools may publish post-initialize names
description string Model-facing description
parameters object JSON Schema (must be an object type)
output_schema object | null API 0.2 schema for required structured_content; forbidden in 0.1
prompt_snippet string | null API 0.4 negotiated tool_prompt_metadata_v1 usage summary; absent/null means no snippet
prompt_guidelines string array API 0.4 negotiated tool_prompt_metadata_v1 usage guidance; omitted defaults to empty
operation object | null API 0.4 negotiated OperationDescriptor; resource-bearing descriptors also require resource_refs_v1

CommandDefinition#

Field Type Description
name string Command name without leading /
description string User-facing summary
usage string | null Compact usage string

ContextContribution#

Field Type Description
label string Stable label for context inspection
content string Plain text sent to the model
placement string "system_prefix", "system_suffix", "prompt_prefix", "prompt_suffix"

ExtensionNotification#

Field Type Description
level string "info", "success", "warning", "error"
title string | null Concise title
message string Notification body

ConfirmationRequest#

Field Type Description
parent_request_id unsigned integer Active owning host request; required in API 0.2, absent in 0.1
prompt string Short action-oriented question
detail string | null Additional scope detail
destructive bool Potentially destructive action
default bool Suggested default

ExtensionInputRequest (API 0.2)#

Field Type Description
parent_request_id unsigned integer Active owning host request
prompt string Non-whitespace frontend prompt, at most 16 KiB UTF-8
secret bool Suppress echo and ordinary editor handling

The response contains exactly value: string|null, with non-null values capped at 256 KiB UTF-8.

ExtensionSecretGetRequest (API 0.2)#

Field Type Description
parent_request_id unsigned integer Active owner-scoped host request
name string Exact manifest-allowlisted identifier, at most 64 ASCII bytes

Success contains exactly value: string, capped at 64 KiB UTF-8. A broker no-value result and provider failure share the generic -32004 unavailable result.

ToolRenderSegment#

Field Type Description
text string Plain text content
style_role string | null Semantic theme role

StatusContribution#

Field Type Description
surface string "status", "header", "footer"
text string Plain display text
style_role string | null Semantic theme role
priority int Higher = retained first when constrained

ExtensionPresentationSnapshot (API 0.2)#

Field Type Description
revision unsigned integer Monotonic within one process generation
status object | null Generic state, compact label, optional detail
activities array Stable ID, kind, state, summary, provenance/timing/references
collection object | null list/tree, stable nodes, selection, selected detail
actions array Stable ID/label routed to a manifest-declared command and literal arguments

See presentation/update for exact state, reference, safety, parentage, and bound rules.

ExtensionMenu (API 0.2)#

Field Type Description
title string | null Display title (≤256 bytes); the host falls back to the extension name
status object | null Generic state, compact label, optional detail, as in presentation snapshots
detail string | null Plain-text explanation (≤16 KiB); its first line is the menu subtitle
items array Top-level ExtensionMenuItems in display order

ExtensionMenuItem:

Field Type Description
id string Unique among siblings (≤256 bytes); keeps the selection stable across refreshes
label string Host-rendered label (≤256 bytes)
description string | null One-line explanation (≤2 KiB)
command string | null Declared command an action runs; absent for a submenu
arguments string array Literal arguments (≤32, each ≤4 KiB)
destructive bool Confirm before running
recommended bool Preselected and emphasized; at most one per level
items array | null Submenu entries; mutually exclusive with command
detail string | null Shown when the submenu opens (≤16 KiB)

The encoded menu is at most 256 KiB.

API 0.2 protocol features#

Feature Required Enables
request_cancellation yes Cooperative $/cancelRequest, cancellation errors, tombstones
content_parts yes Ordered text/media tool-result parts and native result details
request_progress no Request-scoped $/progress
progress_decoration no Bounded ephemeral semantic annotations; also requires request_progress
artifacts no artifact/publish and image/audio content parts
lifecycle_events no Subscribed session/turn/tool observations
policy_intents no Correlated policy/evaluate requests
dynamic_tools no Transactional tools/register, tools/unregister, and revision-pinned tool/call
runtime_commands no Initialize-time authoritative fixed command catalog for compatibility runtimes; no live mutations
agent_sessions conditional Principal/owner-scoped agent/* child model-session service
agent_session_events_v1 API 0.4, conditional Bounded native agent/events cursor observations; requires agent_sessions
agent_session_lifetime_v1 API 0.4, conditional Explicit issued-owner retained child calls and owned-tree agent/stop; requires agent_sessions
agent_model_selection_v1 conditional Bounded agent/models, policy.model_selection, and host-confirmed resolved_model; also requires agent_sessions
delegation_telemetry_v1 conditional first-party requirement Native owner-run AgentEvent::DelegationUpdated child telemetry; required by octet-subagents when agent_sessions is offered
approvals conditional Original-intent/active-owner-bound single-use policy/evaluate retry tokens; also requires policy_intents
secrets conditional Owner-scoped secret/get for exact manifest-allowlisted names
provider_credentials conditional Reviewed provider/credentials resolving exactly one named native provider/model; requires [capabilities].provider_credentials
composer no Host-owned composer/get, composer/set, and composer/insert; the extension API stays 0.2
shortcuts no Runtime shortcut/register plus admitted shortcut/trigger dispatch; at most 64 per process
session_entries no session/append_entry, session/set_name, and session/set_label durable entry/session metadata; declared API 0.4 model-turn/session-operation hooks use the awaited private leaf consumer
session_control_v1 API 0.4, conditional Active-session session/wait_for_idle, session/create, session/fork, session/switch, session/reload; real bound lifecycle driver required
session_compaction_v1 API 0.4, conditional Post-reply, issued-owner session/compact with a durable terminal result; requires session_control_v1 and an opted-in active compaction consumer
message_injection no Bounded session/send_message (assistant
lifecycle_events_v2 no Coalesced message/started, message/updated, message/settled, compaction/*, session/info_changed, dialog/*, model/selected, reasoning/selected, and bash/user fan-out
active_tools no Host-owned tools/set_active replacement active tool set
terminal_handoff no Exclusive, revocable foreground tty grant (terminal/acquire/terminal/release) with terminal/grant-lost revocation

Parent correlation (including input/request), serialized writes, bounded drain, and health tracking are base API 0.2 invariants rather than optional feature strings.


5. Lifecycle#

starting -> initializing -> ready -> draining -> stopped
                              |          |
                              +-> degraded/crashed

/extensions status inspection exposes each running process generation, negotiated features, pending request count, health state, and bounded last error. The supervisor uses backoff after an unexpected exit or terminal transport failure and parked after its retry budget or a permanent manifest/version/re-registration error.

Reload: octet starts and fully negotiates candidate generation N+1 while N remains ready. A process negotiating dynamic_tools may replace its tool catalog; otherwise changed tools, or any changed command/hook/UI contribution, are rejected with re-registration required and require a full host rebuild. octet reserves candidate tool names, marks N draining, waits the bounded drain, cancels the remainder, emits remaining lifecycle terminals, and waits for shutdown acknowledgement or timeout. It then seeds lifecycle state and atomically routes new calls to N+1. If candidate launch or negotiation fails, the old process remains active. Stale progress, child requests, confirmation IDs, approvals, secret lookups, artifacts, presentation snapshots, catalog epochs, and resource handles cannot cross the generation boundary. Unresolved unsafe calls are never replayed. /extensions reload replaces running children; general /reload rebuilds discovery and the product boundary.

Unexpected exit: after one successful initialization, the supervisor removes the dead generation's tool group, enters backoff, and attempts the same candidate-first generation-checked reload. Delay uses full jitter with a 250 ms exponential base and 30 s cap. Eight failed restart attempts park the extension; 30 seconds continuously ready resets that budget. Permanent manifest/version/re-registration errors park immediately. Explicit shutdown cancels supervision, and the reload lock prevents a supervisor/manual reload race. A manual generation change revives the parked watcher. Initial spawn/initialize failures are reported as generation-0 parked discovery entries and are not retried by this post-initialization supervisor. The supervisor does not heartbeat an otherwise live process. A full product rebuild creates a new extension instance and supervisor, resetting in-memory retry/parked state.

API 0.1 has one resident contact policy and no manifest contact-policy field. An enabled, trusted extension is started while the product extension host is constructed only when the resolved product policy is UnsafeHost and the independent process gate permits startup; Controlled retains discovery without a resident child. Each admitted generation remains resident until reload, shutdown, or connection failure; the host supervisor may create a replacement after failure. On reload, the replacement is initialized before the active generation stops admission and shuts down; the replacement is then swapped in. Its wire contract remains frozen and does not emulate API 0.2 stateful guarantees.

API 0.2 lifecycle notifications cover the shared terminal outcome boundary used by interactive, plain, print, RPC, native-host, and Serve execution paths. Every admitted turn is settled across completion, abort, failure, turn limit, frontend stream loss, and shutdown. Observations do not own cleanup.

Sleep inhibition is not a protocol/kernel responsibility, and no core sleep inhibitor remains. The example caffeinate package is an API 0.2, version 0.2.0 extension. It observes owning/root turn/started, turn/settled, and session/settled, reference-counts overlapping turns, runs one bounded macOS helper, and relies on extension shutdown plus process-group cleanup as its final fence.


6. Environment variables#

Every extension child receives:

Variable Value
OCTET_EXTENSION_API_VERSION Exact manifest-selected version ("0.1", "0.2", "0.3", or "0.4")
OCTET_EXTENSION_NAME Extension manifest name
OCTET_EXTENSION_DIR Extension directory (beside manifest)
OCTET_EXTENSION_MANIFEST Absolute path to extension.toml
OCTET_WORKSPACE Active workspace root
OCTET_EXTENSION_SCRATCH Host-owned scratch directory for the active process generation
SSH_AUTH_SOCK Forwarded only for an API 0.2 manifest that explicitly declares it in [capabilities].environment and only when present in the host