diff --git a/.copilot-schema-version b/.copilot-schema-version index eb909693..be1dcc8a 100644 --- a/.copilot-schema-version +++ b/.copilot-schema-version @@ -1 +1 @@ -1.0.71-2 +1.0.73 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cf28b7a1..2a81913f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -25,7 +25,11 @@ When porting features or investigating behavior: 1. **Primary reference**: [nodejs implementation](https://github.com/github/copilot-sdk/tree/main/nodejs) (JavaScript/TypeScript) 2. **Secondary reference**: [python implementation](https://github.com/github/copilot-sdk/tree/main/python) for additional clarity -3. **Local upstream checkout**: The upstream repo is available at `../copilot-sdk` (relative to this repo). +3. **Local upstream checkout**: Resolve the upstream repo with + `bash .github/skills/update-upstream/scripts/resolve-upstream.sh`. This + works from normal checkouts and linked worktrees; set + `COPILOT_SDK_UPSTREAM` when the checkout is not beside the primary + `copilot-sdk-clojure` checkout. 4. **CLI runtime**: The CLI itself is useful for understanding protocol behavior, but the **SDK source of truth** is always the Node.js SDK, not the CLI protocol types. diff --git a/.github/skills/update-upstream/SKILL.md b/.github/skills/update-upstream/SKILL.md index 5c9b4fad..0d75df3c 100644 --- a/.github/skills/update-upstream/SKILL.md +++ b/.github/skills/update-upstream/SKILL.md @@ -1,7 +1,7 @@ --- name: update-upstream -description: Sync the Clojure Copilot SDK with upstream copilot-sdk changes. Runs update.sh, performs gap analysis against Node.js and Python SDKs, ports changes with red/green TDD, runs full CI (E2E tests + examples), gets parallel multi-model code reviews, updates docs, and creates a PR. Use when syncing with new upstream releases or checking for unported changes. -compatibility: Requires copilot CLI authenticated, gh CLI, clojure CLI, bb (babashka). Upstream repo at ../copilot-sdk. +description: Use when syncing the Clojure Copilot SDK with upstream releases or checking for unported Node.js and Python SDK changes. +compatibility: Requires authenticated copilot and gh CLIs, Clojure CLI, bb, and a local github/copilot-sdk checkout beside the primary checkout or specified by COPILOT_SDK_UPSTREAM. --- # Update Upstream Skill @@ -14,19 +14,33 @@ Sync the copilot-sdk-clojure project with upstream [github/copilot-sdk](https:// ### Phase 1: Discovery -1. **Sync local `main` first.** Recently-merged PRs may have already - ported some upstream changes, and your feature branch should sit on - top of the latest `main` to avoid duplicate work and rebase conflicts - later: +1. **Refresh `origin/main` without leaving the current worktree branch.** + Recently merged PRs may have already ported upstream changes: ``` - git checkout main && git pull --ff-only origin main + git fetch origin main + git rev-list --left-right --count HEAD...origin/main ``` - If `main` cannot fast-forward, stop and let the maintainer resolve. -2. Run `./update.sh` from the repo root to pull the latest upstream and list releases. + Never check out `main` inside a linked worktree; the primary checkout may + already have it checked out. If the current branch is only behind, run + `git merge --ff-only origin/main`. If it has diverged, use a fresh project + session from the default branch when available; otherwise ask the + maintainer before integrating `origin/main`. +2. Resolve and fetch the upstream checkout using the tracked, worktree-safe + helper: + ``` + UPSTREAM_REPO="$(bash .github/skills/update-upstream/scripts/resolve-upstream.sh)" && + git -C "$UPSTREAM_REPO" fetch --prune --tags origin + ``` + The helper derives the primary checkout from Git's common directory, so it + works from both normal checkouts and linked worktrees. Set + `COPILOT_SDK_UPSTREAM` to override the sibling checkout location. Shell + tool calls do not share environment, so resolve `UPSTREAM_REPO` again in + each call or chain dependent commands together. 3. Check the current Clojure SDK version in `build.clj` (format: `UPSTREAM.CLJ_PATCH` — see AGENTS.md § Version Management). 4. List upstream commits since our last synced version: ``` - cd ../copilot-sdk && git log --oneline ..HEAD -- nodejs/ + UPSTREAM_REPO="$(bash .github/skills/update-upstream/scripts/resolve-upstream.sh)" && + git -C "$UPSTREAM_REPO" log --oneline ..origin/main -- nodejs/ ``` 5. For each commit, classify: - **Port** — Code changes to `nodejs/src/` (types, client, session, generated) @@ -36,8 +50,8 @@ Sync the copilot-sdk-clojure project with upstream [github/copilot-sdk](https:// Launch three parallel explore agents to build a comprehensive inventory. Use the file mapping in `references/PROJECT.md` to locate the right files. -1. **Node.js SDK** — Read upstream files listed in references/PROJECT.md (types.ts, client.ts, session.ts, index.ts, generated/). Catalog all public types, methods, event types, and event data fields. -2. **Python SDK** — Read `python/copilot/client.py`, `session.py`, `__init__.py`, `generated/`. Note behavioral differences from Node.js. +1. **Node.js SDK** — Resolve `$UPSTREAM_REPO`, then read the upstream files listed in references/PROJECT.md (types.ts, client.ts, session.ts, index.ts, generated/). Catalog all public types, methods, event types, and event data fields. +2. **Python SDK** — Resolve `$UPSTREAM_REPO`, then read `python/copilot/client.py`, `session.py`, `__init__.py`, and `generated/`. Note behavioral differences from Node.js. 3. **Clojure SDK** — Read all `src/github/copilot_sdk/*.clj`. Catalog public functions, specs, event sets, wire conversion. Compare inventories to identify gaps: @@ -111,21 +125,20 @@ At minimum: ### Phase 8: PR Creation -1. **Confirm `main` is current before branching.** Run - `git fetch origin main && git checkout main && git pull --ff-only` if - you haven't refreshed since Phase 1. A stale local `main` causes - rebase conflicts later, especially when prior sync PRs squash-merge. -2. Create a feature branch: `git checkout -b upstream-sync/v` +1. **Confirm the current worktree branch is based on current `origin/main`.** + Run `git fetch origin main` and inspect + `git rev-list --left-right --count HEAD...origin/main`. Do not check out + local `main`. +2. Keep using the project session's existing branch. If running outside a + project worktree and still on the default branch, create a feature branch + with the app-native branch tool when available. 3. Commit changes in logical commits to make them easy to review commit by commit and with descriptive message and `Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>` 4. Push and create PR with `gh pr create` 5. PR body should include: summary, changes list, validation results, review findings table -If the maintainer asks you to rebase a stale branch onto fresh `main`, -expect that previous round-N sync commits on your branch may already be -present in `origin/main` under squash-merge SHAs. Use `git rebase --skip` -for any commit whose patch is already upstream — Git will print -"patch contents already upstream" for the others and drop them -automatically. +If the branch becomes stale after it has commits, do not rewrite history by +default. Prefer a fresh project session from current `main`, or ask the +maintainer before using an additive merge. ### Phase 9: Reflecting on code review feedback. @@ -176,4 +189,6 @@ Real recurring traps when porting upstream changes: 6. **`session.create` and `session.resume` build wire params in two separate functions — keep shared sub-shapes in a named helper.** `build-create-session-params` and `build-resume-session-params` both emit tool defs, system message, provider, MCP servers, custom agents, and commands. A new field on any shape sent by both must be added to both builders, or it ships on create and silently vanishes on resume. Funnel each shared sub-shape through one `*->wire` helper (e.g. `tool-def->wire`, `util/mcp-servers->wire`) rather than duplicating a `cond->` inline. +7. **Sibling repositories must be resolved from Git's common directory, not the worktree root.** In a linked worktree, `../copilot-sdk` points inside the worktree container rather than beside the primary checkout. Always use `scripts/resolve-upstream.sh`; never hard-code an absolute path or derive the sibling from `git rev-parse --show-toplevel`. + For the mechanics of camelCase ↔ kebab-case conversion (including the `?`-suffix rule), see the cheat sheet in `references/PROJECT.md`. diff --git a/.github/skills/update-upstream/references/PROJECT.md b/.github/skills/update-upstream/references/PROJECT.md index 75930570..929f8f8f 100644 --- a/.github/skills/update-upstream/references/PROJECT.md +++ b/.github/skills/update-upstream/references/PROJECT.md @@ -4,11 +4,26 @@ This reference supplements `AGENTS.md` (the canonical project reference) with sy For project structure, testing commands, version format, changelog conventions, and code quality expectations, see `AGENTS.md`. +## Upstream Checkout + +Resolve the local upstream checkout from any normal checkout or linked +worktree: + +```bash +UPSTREAM_REPO="$(bash .github/skills/update-upstream/scripts/resolve-upstream.sh)" +``` + +The helper finds the primary `copilot-sdk-clojure` checkout through Git's +common directory, then resolves its `copilot-sdk` sibling. Set +`COPILOT_SDK_UPSTREAM` when the upstream checkout lives elsewhere. Re-resolve +the variable in each shell tool call because shell environments do not +persist between calls. + ## Upstream ↔ Clojure File Mapping When syncing, map upstream changes to the corresponding Clojure files: -### Upstream (../copilot-sdk) +### Upstream (`$UPSTREAM_REPO`) | Upstream File | Contains | |---------------|----------| diff --git a/.github/skills/update-upstream/scripts/resolve-upstream.sh b/.github/skills/update-upstream/scripts/resolve-upstream.sh new file mode 100755 index 00000000..ecc34ba2 --- /dev/null +++ b/.github/skills/update-upstream/scripts/resolve-upstream.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if (( $# != 0 )); then + echo "usage: $0" >&2 + exit 2 +fi + +common_git_dir="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || { + echo "error: run this script from the copilot-sdk-clojure repository" >&2 + exit 1 +} + +if [[ -n "${COPILOT_SDK_UPSTREAM:-}" ]]; then + candidate="${COPILOT_SDK_UPSTREAM}" +else + primary_checkout="$(dirname "${common_git_dir}")" + candidate="$(dirname "${primary_checkout}")/copilot-sdk" +fi + +upstream_root="$(git -C "${candidate}" rev-parse --show-toplevel 2>/dev/null)" || { + echo "error: upstream github/copilot-sdk checkout not found at ${candidate}" >&2 + echo "clone it beside the primary copilot-sdk-clojure checkout or set COPILOT_SDK_UPSTREAM" >&2 + exit 1 +} + +origin_url="$(git -C "${upstream_root}" remote get-url origin 2>/dev/null)" || { + echo "error: upstream checkout has no origin remote: ${upstream_root}" >&2 + exit 1 +} + +case "${origin_url}" in + git@github.com:github/copilot-sdk.git | \ + ssh://git@github.com/github/copilot-sdk.git | \ + https://github.com/github/copilot-sdk | \ + https://github.com/github/copilot-sdk.git) + ;; + *) + echo "error: expected origin to be github/copilot-sdk, found ${origin_url}" >&2 + echo "set COPILOT_SDK_UPSTREAM to a checkout with the canonical origin remote" >&2 + exit 1 + ;; +esac + +printf '%s\n' "${upstream_root}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 160942cb..34b33019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] +### Changed (agent workflow) +- **Worktree-safe upstream sync** — the repo-local `update-upstream` skill now + uses a tracked helper to resolve the sibling `github/copilot-sdk` checkout + through Git's common directory, and no longer switches to `main`, creates a + second branch, or depends on an ignored root-level `update.sh`. + ### Added (v1.0.7 sync) - **Opaque tool-definition metadata** — port of upstream [PR #1864](https://github.com/github/copilot-sdk/pull/1864). `define-tool` and @@ -33,13 +39,39 @@ All notable changes to this project will be documented in this file. This change `"medium"`, `"high"`, or `"xhigh"`. Session create and resume send it as the exact `reasoningEffort` wire field. When absent, the field is omitted and does not inherit the session-level `:reasoning-effort`. +- **Strongly typed PascalCase `:exp-assignments` contract** — port of + [upstream PR #2033](https://github.com/github/copilot-sdk/pull/2033). Session + create and resume configs validate the complete `CopilotExpAssignmentResponse` + shape and forward its string-keyed PascalCase fields unchanged. +- **`:on-agent-stop` session hook** — port of + [upstream PR #2054](https://github.com/github/copilot-sdk/pull/2054). Session + `:hooks` accept `:on-agent-stop` for runtime `agentStop` callbacks, using the + existing allow/block hook decision contract. +- **Schema regen from 1.0.71-2 through 1.0.73** — port of upstream package bumps + [PR #2035](https://github.com/github/copilot-sdk/pull/2035) and + [PR #2055](https://github.com/github/copilot-sdk/pull/2055). Regenerated wire + specs and coercions. The curated public event sets now include + `:copilot/assistant.server_tool_progress`, + `:copilot/session.managed_settings_enforced`, + `:copilot/session.managed_settings_resolved`, and + `:copilot/tool_search.activated`; `assistant.turn_retry` and + `model.call_start` remain generated internal-only. ### Fixed +- **Hook invocation response envelopes** — `hooks.invoke` now returns the + canonical `HookInvokeResponse` wire shape, wrapping non-nil handler values + under `output`, omitting `output` for nil values, and preserving opaque MCP + metadata within the nested hook output. Unknown session IDs now return an + RPC error instead of a successful nil result. - **Custom-agent MCP server IDs** — [issue #158](https://github.com/copilot-community-sdk/copilot-sdk-clojure/issues/158). Nested `:mcp-servers` now use the same wire serializer as session-level MCP servers on both session create and resume, preserving keyword and string server IDs while converting each server config to the runtime wire shape. +- **Variant-local generated validation** — the Clojure schema generator now + scopes same-named properties with different schemas to each data variant, + preserving `abort`'s closed `reason` enum while `assistant.turn_retry` accepts + open strings. ## [1.0.7-preview.2.1] - 2026-07-15 ### Added diff --git a/doc/api/API.html b/doc/api/API.html index f2f3990d..a3b7856a 100644 --- a/doc/api/API.html +++ b/doc/api/API.html @@ -200,10 +200,10 @@

with-clien :session-limits map (Experimental) Session AI-credit limits. {:max-ai-credits <number>} — serialized as wire sessionLimits.maxAiCredits. (upstream PR #1865) :enable-managed-settings? boolean Opt-in. When true, the runtime self-fetches enterprise managed settings (bypass-permissions policy) at session bootstrap using the session’s :github-token (required; the runtime fails closed if omitted). Gated on some? — an explicit false is forwarded verbatim; an absent key is omitted. Serialized as wire enableManagedSettings. (upstream PR #1925) :canvas-provider map Canvas provider identity for the session. {:id "..." :name "..."} (:name optional) — serialized as wire canvasProvider.{id,name}. (upstream PR #1847) - :exp-assignments map (Internal) Opaque experiment flight assignments. Keys are source-defined flight ids and are forwarded verbatim (string keys bypass kebab→camel conversion). Serialized as expAssignments. (upstream PR #1750) - :mcp-servers map MCP server configs keyed by server ID (see MCP docs). Local (stdio) servers: :mcp-command, :mcp-args, :mcp-tools. Remote (HTTP/SSE) servers: :mcp-server-type (:http/:sse), :mcp-url, :mcp-tools. Spec aliases: ::mcp-stdio-server = ::mcp-local-server, ::mcp-http-server = ::mcp-remote-server + :exp-assignments map (@internal) Exact CopilotExpAssignmentResponse contract using PascalCase string keys. Required: "Features" (string vector), "Flights" (string-to-string map), "Configs" (vector of closed maps containing exactly "Id" (string) and "Parameters" (map of string keys to string, number, boolean, or nil values)), and "AssignmentContext" (string). Optional: "ParameterGroups" (opaque), "FlightingVersion" (number), and "ImpressionId" (string). The Clojure spec rejects unknown top-level and config-entry keys. The map is forwarded unchanged on create-session, resume-session, and join-session (join-session delegates to resume). Serialized as expAssignments. (upstream PR #2033) + :mcp-servers map MCP server configs keyed by opaque string or keyword server IDs; keyword IDs preserve their full spelling without the leading colon (for example, :srv-1 becomes "srv-1" and :team/srv-1 becomes "team/srv-1"). See MCP docs. Local (stdio) servers: :mcp-command, :mcp-args, :mcp-tools. Remote (HTTP/SSE) servers: :mcp-server-type (:http/:sse), :mcp-url, :mcp-tools. Spec aliases: ::mcp-stdio-server = ::mcp-local-server, ::mcp-http-server = ::mcp-remote-server :commands vector Command definitions (slash commands). See Commands - :custom-agents vector Custom agent configs. Each agent map: :agent-name (required), :agent-prompt (required), :agent-display-name, :agent-description, :agent-tools, :agent-infer?, :agent-skills (vector of strings), :agent-model (string, e.g. "claude-haiku-4.5"; when set the runtime tries this model for the agent, falling back to the parent session model — upstream PR #1309), :agent-reasoning-effort ("low", "medium", "high", or "xhigh"), :mcp-servers. :agent-reasoning-effort is serialized as reasoningEffort on both session.create and session.resume. When omitted, no per-agent override is sent; the backend chooses its default rather than inheriting the parent session’s effort. + :custom-agents vector Custom agent configs. Each agent map: :agent-name (required), :agent-prompt (required), :agent-display-name, :agent-description, :agent-tools, :agent-infer?, :agent-skills (vector of strings), :agent-model (string, e.g. "claude-haiku-4.5"; when set the runtime tries this model for the agent, falling back to the parent session model — upstream PR #1309), :agent-reasoning-effort ("low", "medium", "high", or "xhigh"), :mcp-servers. Nested :mcp-servers follow the same config and opaque server-ID rules as session-level MCP servers. :agent-reasoning-effort is serialized as reasoningEffort on both session.create and session.resume. When omitted, no per-agent override is sent; the backend chooses its default rather than inheriting the parent session’s effort. :default-agent map Built-in/default agent config. Use {:excluded-tools [...]} to hide tools from the default agent while leaving them available to custom agents :on-permission-request fn Permission handler function. Optional (upstream PR #1308). When omitted, permission requests are not auto-resolved; resolve them manually via handle-pending-permission-request!. Use copilot/approve-all to approve everything. :streaming? boolean Enable streaming deltas @@ -1178,6 +1178,8 @@

Exported Constants ;; :copilot/command.completed :copilot/commands.changed ;; :copilot/exit_plan_mode.requested :copilot/exit_plan_mode.completed} +

For schema 1.0.73, :copilot/assistant.server_tool_progress also belongs to copilot/assistant-events. :copilot/session.managed_settings_enforced and :copilot/session.managed_settings_resolved belong to copilot/session-events. :copilot/tool_search.activated intentionally belongs only to the master copilot/event-types set, not copilot/interaction-events or copilot/tool-events.

+

The generated wire schemas also contain the internal assistant.turn_retry (additional model inference metadata within an existing turn) and model.call_start (model API dispatch metadata) events. They are wire-only and intentionally excluded from every curated public event set.

evt — Event Keyword Helper

(copilot/evt :session.info)      ;; => :copilot/session.info
 (copilot/evt :assistant.message) ;; => :copilot/assistant.message
@@ -1217,6 +1219,8 @@ 

Event Reference

:copilot/session.session_limits_changed Session limits changed; data: {:session-limits {:max-ai-credits <number>}}, where a nil :session-limits clears the active limits (upstream schema 1.0.67) :copilot/session.usage_checkpoint Durable usage checkpoint for reconstructing aggregate accounting on resume; data: {:total-nano-aiu <number>} with optional :total-premium-requests <number> (upstream schema 1.0.67) :copilot/session.auto_mode_resolved Auto model-selection resolved the model for the first prompt of an auto-mode session; data includes :chosen-model, optional :candidate-models, :category-scores, :confidence, :predicted-label, :reasoning-bucket (experimental; upstream schema 1.0.70-0) + :copilot/session.managed_settings_enforced Experimental ephemeral enforcement of enterprise managed settings for a concrete user- or host-initiated governed action. Data: {:action "bypass_permissions_blocked" :setting <string> :fail-closed <boolean> :message <string>} with optional :escalation in #{"allow_all" "approve_all" "auto_approval" "unrestricted_paths" "unrestricted_urls"}. + :copilot/session.managed_settings_resolved Experimental ephemeral snapshot of effective enterprise managed settings and their authority, emitted when policy is applied or reapplied at session start, on resume, or on account switch. Data: {:source #{"server" "device" "none"} :server-managed <boolean> :device-managed <boolean> :fail-closed <boolean> :bypass-permissions-disabled <boolean> :managed-keys [<string> ...]} with optional opaque JSON :settings. :copilot/session.schedule_rearmed Self-paced schedule re-armed for its next run :copilot/session.binary_asset Canonical bytes for a content-addressed binary asset shared by reference across events :copilot/session.extensions.attachments_pushed Extension pushed attachments into the session @@ -1235,6 +1239,7 @@

Event Reference

:copilot/assistant.usage Token usage for this turn; data may include optional :content-filter-triggered (boolean) and :finish-reason (string) (upstream schema 1.0.63) :copilot/assistant.idle Main agent’s processing loop went idle, including while related background work (running sub-agents or in-flight attached shell commands) is still pending (upstream schema 1.0.66) :copilot/assistant.tool_call_delta Streaming tool-call argument input chunk; data includes :tool-call-id, :input-delta, optional :tool-name, :tool-type (upstream schema 1.0.69-3) + :copilot/assistant.server_tool_progress Ephemeral live progress for a provider-hosted server tool before the finalized serverTools envelope arrives on the terminal assistant.message. Data: {:output-index <integer> :kind <string> :status <string>}; only "web_search" is currently emitted for :kind, and :status is "in_progress", "searching", or "completed". :copilot/model.call_failure Failed LLM API call metadata for telemetry :copilot/abort Current message aborted :copilot/tool.user_requested Tool execution requested by user @@ -1242,6 +1247,7 @@

Event Reference

:copilot/tool.execution_progress Tool execution progress update :copilot/tool.execution_partial_result Tool execution partial result :copilot/tool.execution_complete Tool execution completed; data may include optional :structured-content (arbitrary structured tool result) (upstream schema 1.0.63) + :copilot/tool_search.activated Persisted generic client-side tool activations restored when a session resumes. Data: {:strategy <string> :tool-names [<string> ...]}. :copilot/subagent.started Subagent started; data includes :tool-call-id, :agent-name, :agent-display-name, :agent-description :copilot/subagent.completed Subagent completed; data includes :tool-call-id, :agent-name, :agent-display-name, optional :model, :total-tool-calls, :total-tokens, :duration-ms :copilot/subagent.failed Subagent failed; data includes :tool-call-id, :agent-name, :agent-display-name, :error, optional :model, :total-tool-calls, :total-tokens, :duration-ms @@ -2221,12 +2227,19 @@

Session Hooks

(println "Session ended") nil) + :on-agent-stop + (fn [{:keys [stop-hook-active]} _invocation] + (when-not stop-hook-active + {:decision "block" + :reason "Run the final validation and fix any failures."})) + :on-error-occurred (fn [input invocation] (println "Error:" (:error input)) nil)}}))

All hooks receive an input map (contents vary by hook type) and an invocation map containing {:session-id ...}. Hooks may return nil to proceed normally, or in some cases return a modified value.

+

:on-agent-stop fires when the top-level agent reaches a natural terminal stop. Its input contains base :timestamp (Unix milliseconds) and :cwd (string), SDK-added :session-id, and optional kebab-cased :stop-reason, :transcript-path, and :stop-hook-active; its invocation map is {:session-id ...}. Return {:decision "block" :reason "..."} to keep the agent running and enqueue the reason. Return nil, or throw from the handler, to let the agent stop. When :stop-hook-active is true, a previous block already forced a continuation; use it to avoid indefinite re-blocking. (upstream PR #2054)

Reasoning Effort

For models that support reasoning (like o1), you can control the reasoning effort level:

;; Check model capabilities
diff --git a/doc/reference/API.md b/doc/reference/API.md
index b92f8e8a..4b042508 100644
--- a/doc/reference/API.md
+++ b/doc/reference/API.md
@@ -288,7 +288,7 @@ Create a client and session together, ensuring both are cleaned up on exit.
 | `:session-limits` | map | (Experimental) Session AI-credit limits. `{:max-ai-credits }` — serialized as wire `sessionLimits.maxAiCredits`. (upstream PR #1865) |
 | `:enable-managed-settings?` | boolean | Opt-in. When true, the runtime self-fetches enterprise managed settings (bypass-permissions policy) at session bootstrap using the session's `:github-token` (required; the runtime fails closed if omitted). Gated on `some?` — an explicit `false` is forwarded verbatim; an absent key is omitted. Serialized as wire `enableManagedSettings`. (upstream PR #1925) |
 | `:canvas-provider` | map | Canvas provider identity for the session. `{:id "..." :name "..."}` (`:name` optional) — serialized as wire `canvasProvider.{id,name}`. (upstream PR #1847) |
-| `:exp-assignments` | map | (Internal) Opaque experiment flight assignments. Keys are source-defined flight ids and are forwarded verbatim (string keys bypass kebab→camel conversion). Serialized as `expAssignments`. (upstream PR #1750) |
+| `:exp-assignments` | map | (`@internal`) Exact `CopilotExpAssignmentResponse` contract using PascalCase string keys. Required: `"Features"` (string vector), `"Flights"` (string-to-string map), `"Configs"` (vector of closed maps containing exactly `"Id"` (string) and `"Parameters"` (map of string keys to string, number, boolean, or `nil` values)), and `"AssignmentContext"` (string). Optional: `"ParameterGroups"` (opaque), `"FlightingVersion"` (number), and `"ImpressionId"` (string). The Clojure spec rejects unknown top-level and config-entry keys. The map is forwarded unchanged on `create-session`, `resume-session`, and `join-session` (`join-session` delegates to resume). Serialized as `expAssignments`. ([upstream PR #2033](https://github.com/github/copilot-sdk/pull/2033)) |
 | `:mcp-servers` | map | MCP server configs keyed by opaque string or keyword server IDs; keyword IDs preserve their full spelling without the leading colon (for example, `:srv-1` becomes `"srv-1"` and `:team/srv-1` becomes `"team/srv-1"`). See [MCP docs](../mcp/overview.md). Local (stdio) servers: `:mcp-command`, `:mcp-args`, `:mcp-tools`. Remote (HTTP/SSE) servers: `:mcp-server-type` (`:http`/`:sse`), `:mcp-url`, `:mcp-tools`. Spec aliases: `::mcp-stdio-server` = `::mcp-local-server`, `::mcp-http-server` = `::mcp-remote-server` |
 | `:commands` | vector | Command definitions (slash commands). See [Commands](#commands) |
 | `:custom-agents` | vector | Custom agent configs. Each agent map: `:agent-name` (required), `:agent-prompt` (required), `:agent-display-name`, `:agent-description`, `:agent-tools`, `:agent-infer?`, `:agent-skills` (vector of strings), `:agent-model` (string, e.g. `"claude-haiku-4.5"`; when set the runtime tries this model for the agent, falling back to the parent session model — upstream PR #1309), `:agent-reasoning-effort` (`"low"`, `"medium"`, `"high"`, or `"xhigh"`), `:mcp-servers`. Nested `:mcp-servers` follow the same config and opaque server-ID rules as session-level MCP servers. `:agent-reasoning-effort` is serialized as `reasoningEffort` on both `session.create` and `session.resume`. When omitted, no per-agent override is sent; the backend chooses its default rather than inheriting the parent session's effort. |
@@ -1561,6 +1561,17 @@ copilot/interaction-events
 ;;      :copilot/exit_plan_mode.requested :copilot/exit_plan_mode.completed}
 ```
 
+For schema 1.0.73, `:copilot/assistant.server_tool_progress` also belongs to
+`copilot/assistant-events`. `:copilot/session.managed_settings_enforced` and
+`:copilot/session.managed_settings_resolved` belong to `copilot/session-events`.
+`:copilot/tool_search.activated` intentionally belongs only to the master
+`copilot/event-types` set, not `copilot/interaction-events` or `copilot/tool-events`.
+
+The generated wire schemas also contain the internal `assistant.turn_retry`
+(additional model inference metadata within an existing turn) and
+`model.call_start` (model API dispatch metadata) events. They are wire-only and
+intentionally excluded from every curated public event set.
+
 ### `evt` — Event Keyword Helper
 
 ```clojure
@@ -1602,6 +1613,8 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor
 | `:copilot/session.session_limits_changed` | Session limits changed; data: `{:session-limits {:max-ai-credits }}`, where a `nil` `:session-limits` clears the active limits (upstream schema 1.0.67) |
 | `:copilot/session.usage_checkpoint` | Durable usage checkpoint for reconstructing aggregate accounting on resume; data: `{:total-nano-aiu }` with optional `:total-premium-requests ` (upstream schema 1.0.67) |
 | `:copilot/session.auto_mode_resolved` | Auto model-selection resolved the model for the first prompt of an auto-mode session; data includes `:chosen-model`, optional `:candidate-models`, `:category-scores`, `:confidence`, `:predicted-label`, `:reasoning-bucket` (experimental; upstream schema 1.0.70-0) |
+| `:copilot/session.managed_settings_enforced` | Experimental ephemeral enforcement of enterprise managed settings for a concrete user- or host-initiated governed action. Data: `{:action "bypass_permissions_blocked" :setting  :fail-closed  :message }` with optional `:escalation` in `#{"allow_all" "approve_all" "auto_approval" "unrestricted_paths" "unrestricted_urls"}`. |
+| `:copilot/session.managed_settings_resolved` | Experimental ephemeral snapshot of effective enterprise managed settings and their authority, emitted when policy is applied or reapplied at session start, on resume, or on account switch. Data: `{:source #{"server" "device" "none"} :server-managed  :device-managed  :fail-closed  :bypass-permissions-disabled  :managed-keys [ ...]}` with optional opaque JSON `:settings`. |
 | `:copilot/session.schedule_rearmed` | Self-paced schedule re-armed for its next run |
 | `:copilot/session.binary_asset` | Canonical bytes for a content-addressed binary asset shared by reference across events |
 | `:copilot/session.extensions.attachments_pushed` | Extension pushed attachments into the session |
@@ -1620,6 +1633,7 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor
 | `:copilot/assistant.usage` | Token usage for this turn; data may include optional `:content-filter-triggered` (boolean) and `:finish-reason` (string) (upstream schema 1.0.63) |
 | `:copilot/assistant.idle` | Main agent's processing loop went idle, including while related background work (running sub-agents or in-flight attached shell commands) is still pending (upstream schema 1.0.66) |
 | `:copilot/assistant.tool_call_delta` | Streaming tool-call argument input chunk; data includes `:tool-call-id`, `:input-delta`, optional `:tool-name`, `:tool-type` (upstream schema 1.0.69-3) |
+| `:copilot/assistant.server_tool_progress` | Ephemeral live progress for a provider-hosted server tool before the finalized `serverTools` envelope arrives on the terminal `assistant.message`. Data: `{:output-index  :kind  :status }`; only `"web_search"` is currently emitted for `:kind`, and `:status` is `"in_progress"`, `"searching"`, or `"completed"`. |
 | `:copilot/model.call_failure` | Failed LLM API call metadata for telemetry |
 | `:copilot/abort` | Current message aborted |
 | `:copilot/tool.user_requested` | Tool execution requested by user |
@@ -1627,6 +1641,7 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor
 | `:copilot/tool.execution_progress` | Tool execution progress update |
 | `:copilot/tool.execution_partial_result` | Tool execution partial result |
 | `:copilot/tool.execution_complete` | Tool execution completed; data may include optional `:structured-content` (arbitrary structured tool result) (upstream schema 1.0.63) |
+| `:copilot/tool_search.activated` | Persisted generic client-side tool activations restored when a session resumes. Data: `{:strategy  :tool-names [ ...]}`. |
 | `:copilot/subagent.started` | Subagent started; data includes :tool-call-id, :agent-name, :agent-display-name, :agent-description |
 | `:copilot/subagent.completed` | Subagent completed; data includes :tool-call-id, :agent-name, :agent-display-name, optional :model, :total-tool-calls, :total-tokens, :duration-ms |
 | `:copilot/subagent.failed` | Subagent failed; data includes :tool-call-id, :agent-name, :agent-display-name, :error, optional :model, :total-tool-calls, :total-tokens, :duration-ms |
@@ -2845,6 +2860,12 @@ Lifecycle hooks allow custom logic at various points during the session:
                    (println "Session ended")
                    nil)
 
+                 :on-agent-stop
+                 (fn [{:keys [stop-hook-active]} _invocation]
+                   (when-not stop-hook-active
+                     {:decision "block"
+                      :reason "Run the final validation and fix any failures."}))
+
                  :on-error-occurred
                  (fn [input invocation]
                    (println "Error:" (:error input))
@@ -2855,6 +2876,16 @@ All hooks receive an `input` map (contents vary by hook type) and an `invocation
 containing `{:session-id ...}`. Hooks may return `nil` to proceed normally, or in some
 cases return a modified value.
 
+`:on-agent-stop` fires when the top-level agent reaches a natural terminal stop. Its
+input contains base `:timestamp` (Unix milliseconds) and `:cwd` (string), SDK-added
+`:session-id`, and optional kebab-cased `:stop-reason`, `:transcript-path`, and
+`:stop-hook-active`; its invocation map is `{:session-id ...}`. Return
+`{:decision "block" :reason "..."}` to keep the agent running and enqueue the reason.
+Return `nil`, or throw from the handler, to let the agent stop. When
+`:stop-hook-active` is true, a previous block already forced a continuation; use it to
+avoid indefinite re-blocking.
+([upstream PR #2054](https://github.com/github/copilot-sdk/pull/2054))
+
 ### Reasoning Effort
 
 For models that support reasoning (like o1), you can control the reasoning effort level:
diff --git a/examples/README.md b/examples/README.md
index f245bca5..7ca2bc10 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -716,17 +716,18 @@ clojure -A:examples -X infinite-sessions/run :prompts '["What is Clojure?" "Who
 **Difficulty:** Intermediate  
 **Concepts:** Hooks, callbacks, tool use monitoring
 
-Register callbacks for session lifecycle events: start/end, tool use, prompts, and errors.
+Register callbacks for session lifecycle events: start/end, agent stop, tool use, prompts, and errors.
 
 ### What It Demonstrates
 
-- Configuring `:hooks` in session config with all 6 hook types
+- Configuring seven lifecycle hooks in a session config
 - `:on-session-start` — fires when session begins
 - `:on-session-end` — fires when session ends
 - `:on-pre-tool-use` — fires before a tool runs (return `{:approved true}` to allow)
 - `:on-post-tool-use` — fires after a tool completes
 - `:on-user-prompt-submitted` — fires when user sends a prompt
 - `:on-error-occurred` — fires on errors
+- `:on-agent-stop` — fires when the top-level agent naturally stops; return `nil` to let it stop, or `{:decision "block" :reason "..."}` to request another turn. Use `:stop-hook-active` to avoid blocking repeatedly.
 - Collecting and summarizing hook events
 
 ### Usage
diff --git a/examples/lifecycle_hooks.clj b/examples/lifecycle_hooks.clj
index ef135c21..71e0574e 100644
--- a/examples/lifecycle_hooks.clj
+++ b/examples/lifecycle_hooks.clj
@@ -1,5 +1,5 @@
 (ns lifecycle-hooks
-  "Lifecycle hooks: register callbacks for session start/end, tool use, prompts, and errors."
+  "Lifecycle hooks: register callbacks for session start/end, agent stop, tool use, prompts, and errors."
   (:require [github.copilot-sdk :as copilot]
             [github.copilot-sdk.helpers :as h]))
 
@@ -45,7 +45,15 @@
                         :on-error-occurred
                         (fn [data _ctx]
                           (println "❌ Hook: error-occurred")
-                          (record! :on-error-occurred data))}}]
+                          (record! :on-error-occurred data))
+
+                        :on-agent-stop
+                        (fn [data _ctx]
+                          (println "🛑 Hook: agent-stop")
+                          (record! :on-agent-stop data)
+                          ;; {:decision "block" :reason "..."} keeps the agent running;
+                          ;; use :stop-hook-active to avoid blocking it repeatedly.
+                          nil)}}]
 
       (println "\nPrompt:" prompt "\n")
       (println "🤖:" (h/query prompt :session session))
diff --git a/resources/github/copilot_sdk/api_surface.edn b/resources/github/copilot_sdk/api_surface.edn
index 3154ce9d..f45cc3df 100644
--- a/resources/github/copilot_sdk/api_surface.edn
+++ b/resources/github/copilot_sdk/api_surface.edn
@@ -246,6 +246,9 @@
   :github.copilot-sdk.specs/exit-plan-mode-request
   :github.copilot-sdk.specs/exit-plan-mode-result
   :github.copilot-sdk.specs/exp-assignments
+  :github.copilot-sdk.specs/exp-config-entry
+  :github.copilot-sdk.specs/exp-flag-value
+  :github.copilot-sdk.specs/exp-parameters
   :github.copilot-sdk.specs/exporter-type
   :github.copilot-sdk.specs/extension-id
   :github.copilot-sdk.specs/extension-info
@@ -375,6 +378,7 @@
   :github.copilot-sdk.specs/non-blank-string
   :github.copilot-sdk.specs/notification-queue-size
   :github.copilot-sdk.specs/number
+  :github.copilot-sdk.specs/on-agent-stop
   :github.copilot-sdk.specs/on-auto-mode-switch
   :github.copilot-sdk.specs/on-elicitation-request
   :github.copilot-sdk.specs/on-error-occurred
diff --git a/schemas/README.md b/schemas/README.md
index ba56f8b7..467d0e44 100644
--- a/schemas/README.md
+++ b/schemas/README.md
@@ -4,4 +4,4 @@ These files are fetched verbatim from the `@github/copilot-linux-x64` npm packag
 
 **Do not edit by hand.** To update, run `bb schemas:fetch` after bumping `.copilot-schema-version`.
 
-Currently pinned version: `1.0.71-2`
+Currently pinned version: `1.0.73`
diff --git a/schemas/api.schema.json b/schemas/api.schema.json
index 6686836e..89d05a1f 100644
--- a/schemas/api.schema.json
+++ b/schemas/api.schema.json
@@ -1474,6 +1474,263 @@
         }
       }
     },
+    "factory": {
+      "run": {
+        "rpcMethod": "session.factory.run",
+        "description": "Runs a registered factory by name at the top level.",
+        "params": {
+          "type": "object",
+          "properties": {
+            "sessionId": {
+              "type": "string",
+              "description": "Target session identifier"
+            },
+            "name": {
+              "type": "string",
+              "description": "Registered factory name."
+            },
+            "args": {
+              "description": "Factory input value.",
+              "x-opaque-json": true
+            },
+            "options": {
+              "$ref": "#/definitions/RunOptions",
+              "description": "Factory invocation options."
+            }
+          },
+          "required": [
+            "sessionId",
+            "name",
+            "args"
+          ],
+          "additionalProperties": false,
+          "description": "Parameters for invoking a registered factory.",
+          "title": "FactoryRunRequest",
+          "stability": "experimental"
+        },
+        "result": {
+          "$ref": "#/definitions/FactoryRunResult",
+          "description": "Complete current or terminal factory run envelope."
+        },
+        "stability": "experimental"
+      },
+      "getRun": {
+        "rpcMethod": "session.factory.getRun",
+        "description": "Gets the current or settled envelope for a factory run.",
+        "params": {
+          "type": "object",
+          "properties": {
+            "sessionId": {
+              "type": "string",
+              "description": "Target session identifier"
+            },
+            "runId": {
+              "type": "string",
+              "description": "Factory run identifier."
+            }
+          },
+          "required": [
+            "sessionId",
+            "runId"
+          ],
+          "additionalProperties": false,
+          "description": "Parameters for retrieving a factory run.",
+          "title": "FactoryGetRunRequest",
+          "stability": "experimental"
+        },
+        "result": {
+          "$ref": "#/definitions/FactoryRunResult",
+          "description": "Complete current or terminal factory run envelope."
+        },
+        "stability": "experimental"
+      },
+      "cancel": {
+        "rpcMethod": "session.factory.cancel",
+        "description": "Requests cancellation of a factory run and returns its run envelope.",
+        "params": {
+          "type": "object",
+          "properties": {
+            "sessionId": {
+              "type": "string",
+              "description": "Target session identifier"
+            },
+            "runId": {
+              "type": "string",
+              "description": "Factory run identifier."
+            }
+          },
+          "required": [
+            "sessionId",
+            "runId"
+          ],
+          "additionalProperties": false,
+          "description": "Parameters for cancelling a factory run.",
+          "title": "FactoryCancelRequest",
+          "stability": "experimental"
+        },
+        "result": {
+          "$ref": "#/definitions/FactoryRunResult",
+          "description": "Complete current or terminal factory run envelope."
+        },
+        "stability": "experimental"
+      },
+      "log": {
+        "rpcMethod": "session.factory.log",
+        "description": "Records a batch of ordered factory progress lines.",
+        "params": {
+          "type": "object",
+          "properties": {
+            "sessionId": {
+              "type": "string",
+              "description": "Target session identifier"
+            },
+            "runId": {
+              "type": "string",
+              "description": "Factory run identifier."
+            },
+            "lines": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/FactoryLogLine",
+                "description": "One ordered factory progress line."
+              },
+              "description": "Ordered progress lines to append."
+            }
+          },
+          "required": [
+            "sessionId",
+            "runId",
+            "lines"
+          ],
+          "additionalProperties": false,
+          "description": "Parameters for recording factory progress.",
+          "title": "FactoryLogRequest",
+          "stability": "experimental"
+        },
+        "result": {
+          "$ref": "#/definitions/FactoryAckResult",
+          "description": "Acknowledgement that a factory request was accepted."
+        },
+        "stability": "experimental"
+      },
+      "agent": {
+        "rpcMethod": "session.factory.agent",
+        "description": "Runs one factory-scoped subagent and returns its result.",
+        "params": {
+          "type": "object",
+          "properties": {
+            "sessionId": {
+              "type": "string",
+              "description": "Target session identifier"
+            },
+            "factoryRunId": {
+              "type": "string",
+              "description": "Factory run identifier that owns the subagent."
+            },
+            "prompt": {
+              "type": "string",
+              "description": "Prompt to send to the subagent."
+            },
+            "opts": {
+              "$ref": "#/definitions/FactoryAgentOptions",
+              "description": "Subagent execution options."
+            }
+          },
+          "required": [
+            "sessionId",
+            "factoryRunId",
+            "prompt",
+            "opts"
+          ],
+          "additionalProperties": false,
+          "description": "Parameters for one factory-scoped subagent call.",
+          "title": "FactoryAgentRequest",
+          "stability": "experimental"
+        },
+        "result": {
+          "$ref": "#/definitions/FactoryAgentResult",
+          "description": "Result of one factory-scoped subagent call."
+        },
+        "stability": "experimental"
+      },
+      "journal": {
+        "get": {
+          "rpcMethod": "session.factory.journal.get",
+          "description": "Reads a memoized factory journal entry.",
+          "params": {
+            "type": "object",
+            "properties": {
+              "sessionId": {
+                "type": "string",
+                "description": "Target session identifier"
+              },
+              "runId": {
+                "type": "string",
+                "description": "Factory run identifier."
+              },
+              "key": {
+                "type": "string",
+                "description": "Namespaced journal key."
+              }
+            },
+            "required": [
+              "sessionId",
+              "runId",
+              "key"
+            ],
+            "additionalProperties": false,
+            "description": "Parameters for reading a factory journal entry.",
+            "title": "FactoryJournalGetRequest",
+            "stability": "experimental"
+          },
+          "result": {
+            "$ref": "#/definitions/FactoryJournalGetResult",
+            "description": "Result of reading a factory journal entry."
+          },
+          "stability": "experimental"
+        },
+        "put": {
+          "rpcMethod": "session.factory.journal.put",
+          "description": "Stores a memoized factory journal entry.",
+          "params": {
+            "type": "object",
+            "properties": {
+              "sessionId": {
+                "type": "string",
+                "description": "Target session identifier"
+              },
+              "runId": {
+                "type": "string",
+                "description": "Factory run identifier."
+              },
+              "key": {
+                "type": "string",
+                "description": "Namespaced journal key."
+              },
+              "resultJson": {
+                "description": "JSON result to memoize.",
+                "x-opaque-json": true
+              }
+            },
+            "required": [
+              "sessionId",
+              "runId",
+              "key",
+              "resultJson"
+            ],
+            "additionalProperties": false,
+            "description": "Parameters for storing a factory journal entry.",
+            "title": "FactoryJournalPutRequest",
+            "stability": "experimental"
+          },
+          "result": {
+            "$ref": "#/definitions/FactoryAckResult",
+            "description": "Acknowledgement that a factory request was accepted."
+          },
+          "stability": "experimental"
+        }
+      }
+    },
     "model": {
       "getCurrent": {
         "rpcMethod": "session.model.getCurrent",
@@ -2850,7 +3107,7 @@
       },
       "listTools": {
         "rpcMethod": "session.mcp.listTools",
-        "description": "Lists the tools exposed by a connected MCP server on this session's host.",
+        "description": "Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session.",
         "params": {
           "type": "object",
           "properties": {
@@ -6097,7 +6354,7 @@
       },
       "recordContextChange": {
         "rpcMethod": "session.metadata.recordContextChange",
-        "description": "Records a working-directory/git context change and emits a `session.context_changed` event.",
+        "description": "Records a working-directory/git context change and emits a `session.context_changed` event. For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event): a local session's working directory is authoritative and is moved via `metadata.setWorkingDirectory` (or an SDK `session.resume` that supplies a `workingDirectory`), not by this method.",
         "params": {
           "type": "object",
           "properties": {
@@ -6121,7 +6378,7 @@
         },
         "result": {
           "$ref": "#/definitions/MetadataRecordContextChangeResult",
-          "description": "Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode)."
+          "description": "Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode). For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event); move a local session's working directory via `metadata.setWorkingDirectory` instead."
         },
         "stability": "experimental"
       },
@@ -6986,6 +7243,78 @@
         "stability": "experimental"
       }
     },
+    "factory": {
+      "execute": {
+        "rpcMethod": "factory.execute",
+        "description": "Asks the owning extension connection to execute a registered factory closure.",
+        "params": {
+          "type": "object",
+          "properties": {
+            "sessionId": {
+              "type": "string",
+              "description": "Target session identifier"
+            },
+            "name": {
+              "type": "string",
+              "description": "Registered factory name."
+            },
+            "runId": {
+              "type": "string",
+              "description": "Factory run identifier."
+            },
+            "args": {
+              "description": "Factory input value.",
+              "x-opaque-json": true
+            }
+          },
+          "required": [
+            "sessionId",
+            "name",
+            "runId",
+            "args"
+          ],
+          "additionalProperties": false,
+          "description": "Parameters sent to the owning extension to execute a factory closure.",
+          "title": "FactoryExecuteRequest",
+          "stability": "experimental"
+        },
+        "result": {
+          "$ref": "#/definitions/FactoryExecuteResult",
+          "description": "Result returned by an extension factory closure."
+        },
+        "stability": "experimental"
+      },
+      "abort": {
+        "rpcMethod": "factory.abort",
+        "description": "Asks the owning extension connection to abort a running factory cooperatively.",
+        "params": {
+          "type": "object",
+          "properties": {
+            "sessionId": {
+              "type": "string",
+              "description": "Target session identifier"
+            },
+            "runId": {
+              "type": "string",
+              "description": "Factory run identifier."
+            }
+          },
+          "required": [
+            "sessionId",
+            "runId"
+          ],
+          "additionalProperties": false,
+          "description": "Parameters for cooperatively aborting a factory body.",
+          "title": "FactoryAbortRequest",
+          "stability": "experimental"
+        },
+        "result": {
+          "$ref": "#/definitions/FactoryAckResult",
+          "description": "Acknowledgement that a factory request was accepted."
+        },
+        "stability": "experimental"
+      }
+    },
     "sessionFs": {
       "readFile": {
         "rpcMethod": "sessionFs.readFile",
@@ -7597,6 +7926,24 @@
     }
   },
   "clientGlobal": {
+    "hooks": {
+      "invoke": {
+        "rpcMethod": "hooks.invoke",
+        "description": "Dispatches one SDK callback hook from the runtime to the connection that registered it. Internal transport plumbing: clients opt in through session initialization and the Rust hook processor owns ordering, policy, timeout, and callback routing.",
+        "params": {
+          "$ref": "#/definitions/HookInvokeRequest",
+          "description": "Runtime-owned wire payload for a server-to-client hook callback invocation.",
+          "visibility": "internal"
+        },
+        "result": {
+          "$ref": "#/definitions/HookInvokeResponse",
+          "description": "Optional output returned by an SDK callback hook.",
+          "visibility": "internal"
+        },
+        "stability": "experimental",
+        "visibility": "internal"
+      }
+    },
     "llmInference": {
       "httpRequestStart": {
         "rpcMethod": "llmInference.httpRequestStart",
@@ -11972,140 +12319,609 @@
         "uri",
         "type"
       ],
-      "additionalProperties": false,
-      "description": "Resource link content block referencing an external resource",
-      "title": "ExternalToolTextResultForLlmContentResourceLink"
+      "additionalProperties": false,
+      "description": "Resource link content block referencing an external resource",
+      "title": "ExternalToolTextResultForLlmContentResourceLink"
+    },
+    "ExternalToolTextResultForLlmContentResourceLinkIcon": {
+      "type": "object",
+      "properties": {
+        "src": {
+          "type": "string",
+          "description": "URL or path to the icon image"
+        },
+        "mimeType": {
+          "type": "string",
+          "description": "MIME type of the icon image"
+        },
+        "sizes": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "description": "Available icon sizes (e.g., ['16x16', '32x32'])"
+        },
+        "theme": {
+          "$ref": "#/definitions/ExternalToolTextResultForLlmContentResourceLinkIconTheme",
+          "description": "Theme variant this icon is intended for"
+        }
+      },
+      "required": [
+        "src"
+      ],
+      "additionalProperties": false,
+      "description": "Icon image for a resource",
+      "title": "ExternalToolTextResultForLlmContentResourceLinkIcon"
+    },
+    "ExternalToolTextResultForLlmContentResourceLinkIconTheme": {
+      "type": "string",
+      "enum": [
+        "light",
+        "dark"
+      ],
+      "description": "Theme variant this icon is intended for",
+      "title": "ExternalToolTextResultForLlmContentResourceLinkIconTheme",
+      "x-enumDescriptions": {
+        "light": "Icon intended for light themes.",
+        "dark": "Icon intended for dark themes."
+      }
+    },
+    "ExternalToolTextResultForLlmContentShellExit": {
+      "type": "object",
+      "properties": {
+        "type": {
+          "type": "string",
+          "const": "shell_exit",
+          "description": "Content block type discriminator"
+        },
+        "shellId": {
+          "type": "string",
+          "description": "Shell id, as assigned by Copilot runtime"
+        },
+        "exitCode": {
+          "type": "integer",
+          "description": "Exit code from the completed shell command"
+        },
+        "cwd": {
+          "type": "string",
+          "description": "Working directory where the shell command was executed"
+        },
+        "outputPreview": {
+          "type": "string",
+          "description": "Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output."
+        },
+        "outputTruncated": {
+          "type": "boolean",
+          "description": "Whether outputPreview is known to be incomplete or truncated"
+        }
+      },
+      "required": [
+        "type",
+        "shellId",
+        "exitCode"
+      ],
+      "additionalProperties": false,
+      "description": "Shell command exit metadata with optional output preview",
+      "title": "ExternalToolTextResultForLlmContentShellExit"
+    },
+    "ExternalToolTextResultForLlmContentTerminal": {
+      "type": "object",
+      "properties": {
+        "type": {
+          "type": "string",
+          "const": "terminal",
+          "description": "Content block type discriminator"
+        },
+        "text": {
+          "type": "string",
+          "description": "Terminal/shell output text"
+        },
+        "exitCode": {
+          "type": "integer",
+          "description": "Process exit code, if the command has completed"
+        },
+        "cwd": {
+          "type": "string",
+          "description": "Working directory where the command was executed"
+        }
+      },
+      "required": [
+        "type",
+        "text"
+      ],
+      "additionalProperties": false,
+      "description": "Terminal/shell output content block with optional exit code and working directory",
+      "title": "ExternalToolTextResultForLlmContentTerminal"
+    },
+    "ExternalToolTextResultForLlmContentText": {
+      "type": "object",
+      "properties": {
+        "type": {
+          "type": "string",
+          "const": "text",
+          "description": "Content block type discriminator"
+        },
+        "text": {
+          "type": "string",
+          "description": "The text content"
+        }
+      },
+      "required": [
+        "type",
+        "text"
+      ],
+      "additionalProperties": false,
+      "description": "Plain text content block",
+      "title": "ExternalToolTextResultForLlmContentText"
+    },
+    "FactoryAbortRequest": {
+      "type": "object",
+      "properties": {
+        "runId": {
+          "type": "string",
+          "description": "Factory run identifier."
+        }
+      },
+      "required": [
+        "runId"
+      ],
+      "additionalProperties": false,
+      "description": "Parameters for cooperatively aborting a factory body.",
+      "title": "FactoryAbortRequest",
+      "stability": "experimental"
+    },
+    "FactoryAckResult": {
+      "type": "object",
+      "properties": {},
+      "additionalProperties": false,
+      "description": "Acknowledgement that a factory request was accepted.",
+      "title": "FactoryAckResult"
+    },
+    "FactoryAgentOptions": {
+      "type": "object",
+      "properties": {
+        "label": {
+          "type": "string",
+          "description": "Optional label distinguishing otherwise identical memoized agent calls."
+        },
+        "schema": {
+          "description": "Optional JSON Schema for structured agent output.",
+          "x-opaque-json": true
+        },
+        "model": {
+          "type": "string",
+          "description": "Optional model identifier for the subagent."
+        }
+      },
+      "additionalProperties": false,
+      "description": "Options for one factory-scoped subagent call.",
+      "title": "FactoryAgentOptions",
+      "stability": "experimental"
+    },
+    "FactoryAgentRequest": {
+      "type": "object",
+      "properties": {
+        "factoryRunId": {
+          "type": "string",
+          "description": "Factory run identifier that owns the subagent."
+        },
+        "prompt": {
+          "type": "string",
+          "description": "Prompt to send to the subagent."
+        },
+        "opts": {
+          "$ref": "#/definitions/FactoryAgentOptions",
+          "description": "Subagent execution options."
+        }
+      },
+      "required": [
+        "factoryRunId",
+        "prompt",
+        "opts"
+      ],
+      "additionalProperties": false,
+      "description": "Parameters for one factory-scoped subagent call.",
+      "title": "FactoryAgentRequest",
+      "stability": "experimental"
+    },
+    "FactoryAgentResult": {
+      "type": "object",
+      "properties": {
+        "result": {
+          "description": "Agent result, omitted when the agent produced no result.",
+          "x-opaque-json": true
+        }
+      },
+      "additionalProperties": false,
+      "description": "Result of one factory-scoped subagent call.",
+      "title": "FactoryAgentResult"
+    },
+    "FactoryCancelRequest": {
+      "type": "object",
+      "properties": {
+        "runId": {
+          "type": "string",
+          "description": "Factory run identifier."
+        }
+      },
+      "required": [
+        "runId"
+      ],
+      "additionalProperties": false,
+      "description": "Parameters for cancelling a factory run.",
+      "title": "FactoryCancelRequest",
+      "stability": "experimental"
+    },
+    "FactoryExecuteRequest": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "Registered factory name."
+        },
+        "runId": {
+          "type": "string",
+          "description": "Factory run identifier."
+        },
+        "args": {
+          "description": "Factory input value.",
+          "x-opaque-json": true
+        }
+      },
+      "required": [
+        "name",
+        "runId",
+        "args"
+      ],
+      "additionalProperties": false,
+      "description": "Parameters sent to the owning extension to execute a factory closure.",
+      "title": "FactoryExecuteRequest",
+      "stability": "experimental"
+    },
+    "FactoryExecuteResult": {
+      "type": "object",
+      "properties": {
+        "result": {
+          "description": "Factory result value.",
+          "x-opaque-json": true
+        }
+      },
+      "required": [
+        "result"
+      ],
+      "additionalProperties": false,
+      "description": "Result returned by an extension factory closure.",
+      "title": "FactoryExecuteResult"
+    },
+    "FactoryGetRunRequest": {
+      "type": "object",
+      "properties": {
+        "runId": {
+          "type": "string",
+          "description": "Factory run identifier."
+        }
+      },
+      "required": [
+        "runId"
+      ],
+      "additionalProperties": false,
+      "description": "Parameters for retrieving a factory run.",
+      "title": "FactoryGetRunRequest",
+      "stability": "experimental"
+    },
+    "FactoryJournalGetRequest": {
+      "type": "object",
+      "properties": {
+        "runId": {
+          "type": "string",
+          "description": "Factory run identifier."
+        },
+        "key": {
+          "type": "string",
+          "description": "Namespaced journal key."
+        }
+      },
+      "required": [
+        "runId",
+        "key"
+      ],
+      "additionalProperties": false,
+      "description": "Parameters for reading a factory journal entry.",
+      "title": "FactoryJournalGetRequest",
+      "stability": "experimental"
+    },
+    "FactoryJournalGetResult": {
+      "type": "object",
+      "properties": {
+        "hit": {
+          "type": "boolean",
+          "description": "Whether the journal contained the requested key."
+        },
+        "resultJson": {
+          "description": "Cached JSON result. The hit field distinguishes a cached JSON null from a miss.",
+          "x-opaque-json": true
+        }
+      },
+      "required": [
+        "hit"
+      ],
+      "additionalProperties": false,
+      "description": "Result of reading a factory journal entry.",
+      "title": "FactoryJournalGetResult"
+    },
+    "FactoryJournalPutRequest": {
+      "type": "object",
+      "properties": {
+        "runId": {
+          "type": "string",
+          "description": "Factory run identifier."
+        },
+        "key": {
+          "type": "string",
+          "description": "Namespaced journal key."
+        },
+        "resultJson": {
+          "description": "JSON result to memoize.",
+          "x-opaque-json": true
+        }
+      },
+      "required": [
+        "runId",
+        "key",
+        "resultJson"
+      ],
+      "additionalProperties": false,
+      "description": "Parameters for storing a factory journal entry.",
+      "title": "FactoryJournalPutRequest",
+      "stability": "experimental"
+    },
+    "FactoryLogLine": {
+      "type": "object",
+      "properties": {
+        "seq": {
+          "type": "integer",
+          "minimum": 0,
+          "description": "Monotonic sequence number within the factory run."
+        },
+        "kind": {
+          "$ref": "#/definitions/FactoryLogLineKind",
+          "description": "Progress line kind."
+        },
+        "text": {
+          "type": "string",
+          "description": "Progress text."
+        }
+      },
+      "required": [
+        "seq",
+        "kind",
+        "text"
+      ],
+      "additionalProperties": false,
+      "description": "One ordered factory progress line.",
+      "title": "FactoryLogLine",
+      "stability": "experimental"
+    },
+    "FactoryLogLineKind": {
+      "type": "string",
+      "enum": [
+        "log",
+        "phase"
+      ],
+      "description": "Kind of factory progress line.",
+      "title": "FactoryLogLineKind",
+      "x-enumDescriptions": {
+        "log": "A narrator log line.",
+        "phase": "A named factory phase marker."
+      }
     },
-    "ExternalToolTextResultForLlmContentResourceLinkIcon": {
+    "FactoryLogRequest": {
       "type": "object",
       "properties": {
-        "src": {
+        "runId": {
           "type": "string",
-          "description": "URL or path to the icon image"
-        },
-        "mimeType": {
-          "type": "string",
-          "description": "MIME type of the icon image"
+          "description": "Factory run identifier."
         },
-        "sizes": {
+        "lines": {
           "type": "array",
           "items": {
-            "type": "string"
+            "$ref": "#/definitions/FactoryLogLine",
+            "description": "One ordered factory progress line."
           },
-          "description": "Available icon sizes (e.g., ['16x16', '32x32'])"
-        },
-        "theme": {
-          "$ref": "#/definitions/ExternalToolTextResultForLlmContentResourceLinkIconTheme",
-          "description": "Theme variant this icon is intended for"
+          "description": "Ordered progress lines to append."
         }
       },
       "required": [
-        "src"
+        "runId",
+        "lines"
       ],
       "additionalProperties": false,
-      "description": "Icon image for a resource",
-      "title": "ExternalToolTextResultForLlmContentResourceLinkIcon"
+      "description": "Parameters for recording factory progress.",
+      "title": "FactoryLogRequest",
+      "stability": "experimental"
     },
-    "ExternalToolTextResultForLlmContentResourceLinkIconTheme": {
+    "FactoryRunFailure": {
+      "anyOf": [
+        {
+          "type": "object",
+          "properties": {
+            "kind": {
+              "$ref": "#/definitions/FactoryRunFailureKind",
+              "description": "Resource ceiling that stopped the run."
+            },
+            "value": {
+              "type": "number",
+              "exclusiveMinimum": 0,
+              "description": "Approved effective ceiling that was reached."
+            },
+            "runId": {
+              "type": "string",
+              "description": "Factory run identifier."
+            },
+            "type": {
+              "type": "string",
+              "const": "factory_limit_reached"
+            }
+          },
+          "required": [
+            "type",
+            "kind",
+            "value",
+            "runId"
+          ],
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "properties": {
+            "runId": {
+              "type": "string",
+              "description": "Factory run identifier whose changed limits were declined."
+            },
+            "reason": {
+              "type": "string",
+              "description": "Human-readable reason the resume did not proceed."
+            },
+            "type": {
+              "type": "string",
+              "const": "factory_resume_declined"
+            }
+          },
+          "required": [
+            "type",
+            "runId",
+            "reason"
+          ],
+          "additionalProperties": false
+        }
+      ],
+      "description": "Machine-readable factory run failure.",
+      "title": "FactoryRunFailure"
+    },
+    "FactoryRunFailureKind": {
       "type": "string",
       "enum": [
-        "light",
-        "dark"
+        "maxTotalSubagents",
+        "timeout"
       ],
-      "description": "Theme variant this icon is intended for",
-      "title": "ExternalToolTextResultForLlmContentResourceLinkIconTheme",
+      "description": "Cumulative resource ceiling that stopped a factory run.",
+      "title": "FactoryRunFailureKind",
       "x-enumDescriptions": {
-        "light": "Icon intended for light themes.",
-        "dark": "Icon intended for dark themes."
+        "maxTotalSubagents": "The run admitted the approved maximum total number of subagents.",
+        "timeout": "The run reached the approved timeout deadline."
       }
     },
-    "ExternalToolTextResultForLlmContentShellExit": {
+    "FactoryRunLimits": {
       "type": "object",
       "properties": {
-        "type": {
-          "type": "string",
-          "const": "shell_exit",
-          "description": "Content block type discriminator"
-        },
-        "shellId": {
-          "type": "string",
-          "description": "Shell id, as assigned by Copilot runtime"
-        },
-        "exitCode": {
+        "maxConcurrentSubagents": {
           "type": "integer",
-          "description": "Exit code from the completed shell command"
-        },
-        "cwd": {
-          "type": "string",
-          "description": "Working directory where the shell command was executed"
+          "minimum": 0,
+          "exclusiveMinimum": 0,
+          "description": "Maximum number of factory subagents that may run concurrently."
         },
-        "outputPreview": {
-          "type": "string",
-          "description": "Output associated with this shell command, if available. May be partial, truncated, or a preview; not guaranteed to be full output."
+        "maxTotalSubagents": {
+          "type": "integer",
+          "minimum": 0,
+          "exclusiveMinimum": 0,
+          "description": "Maximum total number of factory subagents that may be admitted."
         },
-        "outputTruncated": {
-          "type": "boolean",
-          "description": "Whether outputPreview is known to be incomplete or truncated"
+        "timeout": {
+          "type": "number",
+          "exclusiveMinimum": 0,
+          "description": "Factory active-run timeout in milliseconds."
         }
       },
-      "required": [
-        "type",
-        "shellId",
-        "exitCode"
-      ],
       "additionalProperties": false,
-      "description": "Shell command exit metadata with optional output preview",
-      "title": "ExternalToolTextResultForLlmContentShellExit"
+      "description": "Wire-only per-invocation factory resource ceiling overrides.",
+      "title": "FactoryRunLimits",
+      "stability": "experimental"
     },
-    "ExternalToolTextResultForLlmContentTerminal": {
+    "FactoryRunRequest": {
       "type": "object",
       "properties": {
-        "type": {
-          "type": "string",
-          "const": "terminal",
-          "description": "Content block type discriminator"
-        },
-        "text": {
+        "name": {
           "type": "string",
-          "description": "Terminal/shell output text"
+          "description": "Registered factory name."
         },
-        "exitCode": {
-          "type": "integer",
-          "description": "Process exit code, if the command has completed"
+        "args": {
+          "description": "Factory input value.",
+          "x-opaque-json": true
         },
-        "cwd": {
-          "type": "string",
-          "description": "Working directory where the command was executed"
+        "options": {
+          "$ref": "#/definitions/RunOptions",
+          "description": "Factory invocation options."
         }
       },
       "required": [
-        "type",
-        "text"
+        "name",
+        "args"
       ],
       "additionalProperties": false,
-      "description": "Terminal/shell output content block with optional exit code and working directory",
-      "title": "ExternalToolTextResultForLlmContentTerminal"
+      "description": "Parameters for invoking a registered factory.",
+      "title": "FactoryRunRequest",
+      "stability": "experimental"
     },
-    "ExternalToolTextResultForLlmContentText": {
+    "FactoryRunResult": {
       "type": "object",
       "properties": {
-        "type": {
+        "runId": {
           "type": "string",
-          "const": "text",
-          "description": "Content block type discriminator"
+          "description": "Factory run identifier."
         },
-        "text": {
+        "status": {
+          "$ref": "#/definitions/FactoryRunStatus",
+          "description": "Current or terminal factory run status."
+        },
+        "result": {
+          "description": "Completed factory result.",
+          "x-opaque-json": true
+        },
+        "error": {
           "type": "string",
-          "description": "The text content"
+          "description": "Error message for an errored run."
+        },
+        "failure": {
+          "$ref": "#/definitions/FactoryRunFailure",
+          "description": "Machine-readable failure details for an errored run."
+        },
+        "reason": {
+          "type": "string",
+          "description": "Reason for a halted or cancelled run."
+        },
+        "snapshot": {
+          "description": "Partial journal and progress snapshot for a halted, cancelled, or errored run.",
+          "x-opaque-json": true
         }
       },
       "required": [
-        "type",
-        "text"
+        "runId",
+        "status"
       ],
       "additionalProperties": false,
-      "description": "Plain text content block",
-      "title": "ExternalToolTextResultForLlmContentText"
+      "description": "Complete current or terminal factory run envelope.",
+      "title": "FactoryRunResult"
+    },
+    "FactoryRunStatus": {
+      "type": "string",
+      "enum": [
+        "pending",
+        "running",
+        "completed",
+        "halted",
+        "cancelled",
+        "error"
+      ],
+      "description": "Current or terminal state of a factory run.",
+      "title": "FactoryRunStatus",
+      "x-enumDescriptions": {
+        "pending": "The run was minted and is awaiting approval.",
+        "running": "The run is executing.",
+        "completed": "The run completed successfully.",
+        "halted": "The run was interrupted while resource budget remained.",
+        "cancelled": "The run was cancelled before completion.",
+        "error": "The factory body failed or reached a cumulative resource ceiling."
+      }
     },
     "FilterMapping": {
       "anyOf": [
@@ -12650,6 +13466,85 @@
       "description": "Authentication-info variant for GitHub-internal HMAC auth, carrying the public GitHub host and HMAC secret.",
       "title": "HMACAuthInfo"
     },
+    "HookInvokeRequest": {
+      "type": "object",
+      "properties": {
+        "sessionId": {
+          "type": "string"
+        },
+        "hookType": {
+          "$ref": "#/definitions/HookType"
+        },
+        "input": {
+          "x-opaque-json": true
+        }
+      },
+      "required": [
+        "sessionId",
+        "hookType",
+        "input"
+      ],
+      "additionalProperties": false,
+      "description": "Runtime-owned wire payload for a server-to-client hook callback invocation.",
+      "title": "HookInvokeRequest",
+      "visibility": "internal"
+    },
+    "HookInvokeResponse": {
+      "type": "object",
+      "properties": {
+        "output": {
+          "x-opaque-json": true
+        }
+      },
+      "additionalProperties": false,
+      "description": "Optional output returned by an SDK callback hook.",
+      "title": "HookInvokeResponse",
+      "visibility": "internal"
+    },
+    "HookType": {
+      "type": "string",
+      "enum": [
+        "preToolUse",
+        "preMcpToolCall",
+        "postToolUse",
+        "postToolUseFailure",
+        "userPromptSubmitted",
+        "userPromptTransformed",
+        "sessionStart",
+        "sessionEnd",
+        "postResult",
+        "prePRDescription",
+        "errorOccurred",
+        "agentStop",
+        "subagentStart",
+        "subagentStop",
+        "preCompact",
+        "permissionRequest",
+        "notification"
+      ],
+      "description": "Hook event name dispatched through the SDK callback transport.",
+      "title": "HookType",
+      "visibility": "internal",
+      "x-enumDescriptions": {
+        "preToolUse": "Runs before a tool is invoked.",
+        "preMcpToolCall": "Runs before an MCP tool is invoked.",
+        "postToolUse": "Runs after a tool completes successfully.",
+        "postToolUseFailure": "Runs after a tool fails.",
+        "userPromptSubmitted": "Runs after the user submits a prompt.",
+        "userPromptTransformed": "Runs after the runtime transforms the submitted prompt for the model, before it is added to session history.",
+        "sessionStart": "Runs when a session starts.",
+        "sessionEnd": "Runs when a session ends.",
+        "postResult": "Runs after an agent result is produced.",
+        "prePRDescription": "Runs before a pull request description is generated.",
+        "errorOccurred": "Runs when the agent encounters an error.",
+        "agentStop": "Runs when the agent stops.",
+        "subagentStart": "Runs when a subagent starts.",
+        "subagentStop": "Runs when a subagent stops.",
+        "preCompact": "Runs before conversation context is compacted.",
+        "permissionRequest": "Runs when the agent requests permission.",
+        "notification": "Runs when the agent emits a notification."
+      }
+    },
     "InstalledPlugin": {
       "type": "object",
       "properties": {
@@ -13099,6 +13994,10 @@
         "cancelReason": {
           "type": "string",
           "description": "Optional human-readable reason for the cancellation, propagated for logging."
+        },
+        "agentInvocationId": {
+          "type": "string",
+          "description": "Identity of the agent invocation (one agentic loop) this body chunk belongs to, matching the `agentInvocationId` semantics on httpRequestStart. Carried per chunk so a persistent transport can attribute successive turns correctly: when a WebSocket connection is reused across turns, the httpRequestStart identity reflects only the turn that opened the connection, so each later turn stamps its own invocation id here. Absent when the runtime has no invocation context for the request, or on the plain-HTTP transport where every request has its own httpRequestStart."
         }
       },
       "required": [
@@ -13144,11 +14043,15 @@
         },
         "agentId": {
           "type": "string",
-          "description": "Stable per-agent-instance id attributing this request to a specific agent trajectory. Present when the request originates from an agent turn; absent for requests issued outside any agent context (e.g. some SDK callers). A request with an `agentId` but no `parentAgentId` is a root-agent request; one carrying both is a subagent request. Sourced from the runtime's per-request agent context and surfaced on the envelope independently of transport, so it is available for both first-party (CAPI) and BYOK/custom-provider requests; on the CAPI transport the runtime derives the upstream `X-Agent-Task-Id` header from this same context. Consumers routing each provider call to a training trajectory should key on this rather than on lifecycle events, since it is available on the request path before sampling."
+          "description": "Stable identity of the agent trajectory that issued this request. Present when the request originates from an agent turn; absent for requests outside any agent context. This is the same identity used by lifecycle and bridged session events and remains constant across turns and retries."
         },
         "parentAgentId": {
           "type": "string",
-          "description": "Id of the parent agent that spawned the agent issuing this request. Present only for subagent requests; absent for root-agent requests and non-agent requests. Combined with `agentId`, this lets consumers attribute a call to a child trajectory versus the root. Like `agentId`, it comes from the runtime's per-request agent context independently of transport; on the CAPI transport the runtime derives the upstream `X-Parent-Agent-Id` header from this same context."
+          "description": "Stable identity of the immediate parent trajectory. Present for child trajectories such as subagents and conversation-sampling requests; absent for root-agent and non-agent requests."
+        },
+        "agentInvocationId": {
+          "type": "string",
+          "description": "Identity of the agent invocation (one agentic loop) that issued this request. It remains fixed across physical retries within the invocation and is distinct from the stable trajectory `agentId`. A caller-supplied invocation id always takes precedence (this covers auxiliary calls that have no model call id). Otherwise, first-party CAPI requests fall back to the runtime's agent task id — the same value the runtime emits as the `X-Agent-Task-Id` header — while custom-provider requests fall back to the model call id."
         },
         "interactionType": {
           "type": "string",
@@ -14625,7 +15528,7 @@
           "type": "array",
           "items": {
             "$ref": "#/definitions/McpTools",
-            "description": "MCP tool metadata with tool name and optional description."
+            "description": "MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata."
           },
           "description": "Tools exposed by the server."
         }
@@ -15739,15 +16642,52 @@
         "description": {
           "type": "string",
           "description": "Tool description, when provided."
+        },
+        "ui": {
+          "$ref": "#/definitions/McpToolUi",
+          "description": "Normalized MCP Apps discovery metadata. An empty object indicates that a valid `_meta.ui` block was present without recognized fields."
         }
       },
       "required": [
         "name"
       ],
       "additionalProperties": false,
-      "description": "MCP tool metadata with tool name and optional description.",
+      "description": "MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata.",
       "title": "McpTools"
     },
+    "McpToolUi": {
+      "type": "object",
+      "properties": {
+        "resourceUri": {
+          "type": "string",
+          "description": "URI of the tool's MCP App resource, typically a `ui://` resource identifier. Use `session.mcp.resources.read` to fetch its HTML and resource metadata."
+        },
+        "visibility": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/McpToolUiVisibility",
+            "description": "Consumer allowed to call an MCP tool."
+          },
+          "description": "Tool visibility advertised by the server. When absent, MCP Apps defaults apply."
+        }
+      },
+      "additionalProperties": false,
+      "description": "Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block.",
+      "title": "McpToolUi"
+    },
+    "McpToolUiVisibility": {
+      "type": "string",
+      "enum": [
+        "model",
+        "app"
+      ],
+      "description": "Consumer allowed to call an MCP tool.",
+      "title": "McpToolUiVisibility",
+      "x-enumDescriptions": {
+        "model": "The model may call the tool.",
+        "app": "An MCP App view may call the tool."
+      }
+    },
     "McpUnregisterExternalClientRequest": {
       "type": "object",
       "properties": {
@@ -15954,7 +16894,7 @@
       "type": "object",
       "properties": {},
       "additionalProperties": false,
-      "description": "Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode).",
+      "description": "Notify the session that its working directory context has changed. Emits a `session.context_changed` event so consumers (telemetry, OTel tracker, ACP, the timeline UI) can react. Use this when the host has detected a cwd/branch/repo change outside the session's normal lifecycle (e.g., after a shell command in interactive mode). For a local session, a report whose `cwd` diverges from the session's current working directory is ignored (the call still succeeds but records nothing and emits no event); move a local session's working directory via `metadata.setWorkingDirectory` instead.",
       "title": "MetadataRecordContextChangeResult"
     },
     "MetadataSetWorkingDirectoryRequest": {
@@ -21347,6 +22287,23 @@
       "description": "Repository context for the remote session.",
       "title": "RemoteSessionRepository"
     },
+    "RunOptions": {
+      "type": "object",
+      "properties": {
+        "limits": {
+          "$ref": "#/definitions/FactoryRunLimits",
+          "description": "Per-invocation resource ceiling overrides."
+        },
+        "resumeFromRunId": {
+          "type": "string",
+          "description": "Run identifier whose journal and progress should seed this resumed run."
+        }
+      },
+      "additionalProperties": false,
+      "description": "Options controlling factory invocation.",
+      "title": "RunOptions",
+      "stability": "experimental"
+    },
     "SandboxConfig": {
       "type": "object",
       "properties": {
@@ -21361,6 +22318,14 @@
         "addCurrentWorkingDirectory": {
           "type": "boolean",
           "description": "Whether to auto-add the current working directory to readwritePaths. Default: true."
+        },
+        "gitAuth": {
+          "type": "boolean",
+          "description": "Whether to inject the Copilot GitHub token as an `http..extraheader` so authenticated HTTPS git works inside the sandbox without the shell-based credential helper the sandbox blocks. Default: false (opt-in)."
+        },
+        "ghAuth": {
+          "type": "boolean",
+          "description": "Whether to export `GH_TOKEN` so the `gh` CLI authenticates inside the sandbox without the OS keyring the sandbox blocks. Default: false (opt-in)."
         }
       },
       "required": [
@@ -21969,6 +22934,13 @@
             "description": "Server-side skill metadata, including name, description, source, enabled/invocable state, path, project path, and argument hint."
           },
           "description": "All discovered skills across all sources"
+        },
+        "errors": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "description": "Messages for skills that failed to load (e.g. malformed SKILL.md). Empty when host skills are excluded so host-local paths are not disclosed to multitenant callers."
         }
       },
       "required": [
@@ -23267,6 +24239,14 @@
           },
           "description": "Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`)."
         },
+        "modelPriceCategories": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/SessionModelPriceCategory",
+            "description": "Cost-category metadata for a CAPI model."
+          },
+          "description": "Cost categories for the full CAPI catalog, including picker-disabled models that Auto may select. Metadata only; entries absent from `list` are not manually selectable."
+        },
         "quotaSnapshots": {
           "type": "object",
           "additionalProperties": {
@@ -23282,6 +24262,24 @@
       "description": "The list of models available to this session.",
       "title": "SessionModelList"
     },
+    "SessionModelPriceCategory": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string"
+        },
+        "priceCategory": {
+          "$ref": "#/definitions/ModelPickerPriceCategory"
+        }
+      },
+      "required": [
+        "id",
+        "priceCategory"
+      ],
+      "additionalProperties": false,
+      "description": "Cost-category metadata for a CAPI model.",
+      "title": "SessionModelPriceCategory"
+    },
     "SessionOpenOptions": {
       "type": "object",
       "properties": {
@@ -25333,6 +26331,11 @@
         "success": {
           "type": "boolean",
           "description": "Whether the operation succeeded"
+        },
+        "pluginHookCount": {
+          "type": "integer",
+          "minimum": 0,
+          "description": "Number of hooks loaded from installed plugins, returned when installedPlugins is updated"
         }
       },
       "required": [
@@ -28100,6 +29103,11 @@
           "$ref": "#/definitions/UsageMetricsModelMetricUsage",
           "description": "Token usage metrics for this model"
         },
+        "cacheExpiresAt": {
+          "type": "string",
+          "format": "date-time",
+          "description": "Latest known prompt-cache expiration for this model. A timestamp in the past indicates that the observed cache has expired."
+        },
         "totalNanoAiu": {
           "type": "number",
           "minimum": 0,
diff --git a/schemas/session-events.schema.json b/schemas/session-events.schema.json
index 42ef8e75..fa3e01b2 100644
--- a/schemas/session-events.schema.json
+++ b/schemas/session-events.schema.json
@@ -775,6 +775,87 @@
       "description": "Session event \"assistant.reasoning\". Assistant reasoning content for timeline display with complete thinking text",
       "title": "AssistantReasoningEvent"
     },
+    "AssistantServerToolProgressData": {
+      "type": "object",
+      "properties": {
+        "outputIndex": {
+          "type": "integer",
+          "description": "Position of the hosted tool call in the response output. Stable across the call's lifecycle events (unlike the provider's per-event item id, which CAPI rotates), so the host keys the live in-progress row on it."
+        },
+        "kind": {
+          "type": "string",
+          "description": "Kind of hosted server tool that is running. Only `web_search` is emitted today."
+        },
+        "status": {
+          "type": "string",
+          "description": "Lifecycle status of the hosted call: `in_progress`, `searching`, or `completed`."
+        }
+      },
+      "required": [
+        "outputIndex",
+        "kind",
+        "status"
+      ],
+      "additionalProperties": false,
+      "description": "Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message",
+      "title": "AssistantServerToolProgressData"
+    },
+    "AssistantServerToolProgressEvent": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid",
+          "description": "Unique event identifier (UUID v4), generated when the event is emitted"
+        },
+        "timestamp": {
+          "type": "string",
+          "format": "date-time",
+          "description": "ISO 8601 timestamp when the event was created"
+        },
+        "parentId": {
+          "anyOf": [
+            {
+              "type": "string",
+              "format": "uuid"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "description": "ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event."
+        },
+        "ephemeral": {
+          "type": "boolean",
+          "const": true,
+          "description": "Always true for events that are transient and not persisted to the session event log on disk."
+        },
+        "agentId": {
+          "type": "string",
+          "description": "Sub-agent instance identifier. Absent for events from the root/main agent and session-level events."
+        },
+        "type": {
+          "type": "string",
+          "const": "assistant.server_tool_progress",
+          "description": "Type discriminator. Always \"assistant.server_tool_progress\"."
+        },
+        "data": {
+          "$ref": "#/definitions/AssistantServerToolProgressData",
+          "description": "Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message"
+        }
+      },
+      "required": [
+        "id",
+        "timestamp",
+        "parentId",
+        "ephemeral",
+        "type",
+        "data"
+      ],
+      "additionalProperties": false,
+      "description": "Session event \"assistant.server_tool_progress\". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message",
+      "title": "AssistantServerToolProgressEvent"
+    },
     "AssistantStreamingDeltaData": {
       "type": "object",
       "properties": {
@@ -1004,6 +1085,86 @@
       "description": "Session event \"assistant.turn_end\". Turn completion metadata including the turn identifier",
       "title": "AssistantTurnEndEvent"
     },
+    "AssistantTurnRetryData": {
+      "type": "object",
+      "properties": {
+        "turnId": {
+          "type": "string",
+          "description": "Identifier of the turn whose model inference is being retried"
+        },
+        "model": {
+          "type": "string",
+          "description": "Model identifier used for this retry, when known"
+        },
+        "reason": {
+          "type": "string",
+          "description": "Provider or runtime classification that caused the retry, when known"
+        }
+      },
+      "required": [
+        "turnId"
+      ],
+      "additionalProperties": false,
+      "description": "Metadata for an additional model inference attempt within an existing assistant turn",
+      "title": "AssistantTurnRetryData"
+    },
+    "AssistantTurnRetryEvent": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid",
+          "description": "Unique event identifier (UUID v4), generated when the event is emitted"
+        },
+        "timestamp": {
+          "type": "string",
+          "format": "date-time",
+          "description": "ISO 8601 timestamp when the event was created"
+        },
+        "parentId": {
+          "anyOf": [
+            {
+              "type": "string",
+              "format": "uuid"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "description": "ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event."
+        },
+        "ephemeral": {
+          "type": "boolean",
+          "const": true,
+          "description": "Always true for events that are transient and not persisted to the session event log on disk."
+        },
+        "agentId": {
+          "type": "string",
+          "description": "Sub-agent instance identifier. Absent for events from the root/main agent and session-level events."
+        },
+        "type": {
+          "type": "string",
+          "const": "assistant.turn_retry",
+          "description": "Type discriminator. Always \"assistant.turn_retry\"."
+        },
+        "data": {
+          "$ref": "#/definitions/AssistantTurnRetryData",
+          "description": "Metadata for an additional model inference attempt within an existing assistant turn"
+        }
+      },
+      "required": [
+        "id",
+        "timestamp",
+        "parentId",
+        "ephemeral",
+        "type",
+        "data"
+      ],
+      "additionalProperties": false,
+      "description": "Session event \"assistant.turn_retry\". Metadata for an additional model inference attempt within an existing assistant turn",
+      "title": "AssistantTurnRetryEvent",
+      "visibility": "internal"
+    },
     "AssistantTurnStartData": {
       "type": "object",
       "properties": {
@@ -1183,6 +1344,11 @@
           "minimum": 0,
           "description": "Number of tokens written to prompt cache"
         },
+        "cacheExpiresAt": {
+          "type": "string",
+          "format": "date-time",
+          "description": "Updated prompt-cache expiration for this model call. Present only when the call establishes or refreshes known cache state."
+        },
         "reasoningTokens": {
           "type": "integer",
           "minimum": 0,
@@ -5903,6 +6069,26 @@
         "local": "The handoff originated from a local session."
       }
     },
+    "HeaderEntry": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "HTTP response header name as observed by the runtime."
+        },
+        "value": {
+          "type": "string",
+          "description": "HTTP response header value as observed by the runtime."
+        }
+      },
+      "required": [
+        "name",
+        "value"
+      ],
+      "additionalProperties": false,
+      "description": "Single HTTP header entry as a name/value pair.",
+      "title": "HeaderEntry"
+    },
     "HookEndData": {
       "type": "object",
       "properties": {
@@ -6241,28 +6427,252 @@
           "type": "string",
           "description": "Category of informational message (e.g., \"notification\", \"timing\", \"context_window\", \"mcp\", \"snapshot\", \"configuration\", \"authentication\", \"model\")"
         },
-        "message": {
-          "type": "string",
-          "description": "Human-readable informational message for display in the timeline"
+        "message": {
+          "type": "string",
+          "description": "Human-readable informational message for display in the timeline"
+        },
+        "url": {
+          "type": "string",
+          "description": "Optional URL associated with this message that the user can open in a browser"
+        },
+        "tip": {
+          "type": "string",
+          "description": "Optional actionable tip displayed with this message"
+        }
+      },
+      "required": [
+        "infoType",
+        "message"
+      ],
+      "additionalProperties": false,
+      "description": "Informational message for timeline display with categorization",
+      "title": "InfoData"
+    },
+    "InfoEvent": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid",
+          "description": "Unique event identifier (UUID v4), generated when the event is emitted"
+        },
+        "timestamp": {
+          "type": "string",
+          "format": "date-time",
+          "description": "ISO 8601 timestamp when the event was created"
+        },
+        "parentId": {
+          "anyOf": [
+            {
+              "type": "string",
+              "format": "uuid"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "description": "ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event."
+        },
+        "ephemeral": {
+          "type": "boolean",
+          "description": "When true, the event is transient and not persisted to the session event log on disk"
+        },
+        "agentId": {
+          "type": "string",
+          "description": "Sub-agent instance identifier. Absent for events from the root/main agent and session-level events."
+        },
+        "type": {
+          "type": "string",
+          "const": "session.info",
+          "description": "Type discriminator. Always \"session.info\"."
+        },
+        "data": {
+          "$ref": "#/definitions/InfoData",
+          "description": "Informational message for timeline display with categorization"
+        }
+      },
+      "required": [
+        "id",
+        "timestamp",
+        "parentId",
+        "type",
+        "data"
+      ],
+      "additionalProperties": false,
+      "description": "Session event \"session.info\". Informational message for timeline display with categorization",
+      "title": "InfoEvent"
+    },
+    "ManagedSettingsEnforcedAction": {
+      "type": "string",
+      "enum": [
+        "bypass_permissions_blocked"
+      ],
+      "description": "The category of runtime action that enterprise managed settings governed (blocked or capped)",
+      "title": "ManagedSettingsEnforcedAction",
+      "x-enumDescriptions": {
+        "bypass_permissions_blocked": "An attempt to turn on a bypass-permissions (\"yolo\") escalation was refused or capped because policy disables bypass-permissions mode."
+      }
+    },
+    "ManagedSettingsEnforcedData": {
+      "type": "object",
+      "properties": {
+        "action": {
+          "$ref": "#/definitions/ManagedSettingsEnforcedAction",
+          "description": "The category of runtime action that managed policy governed."
+        },
+        "escalation": {
+          "$ref": "#/definitions/ManagedSettingsEnforcedEscalation",
+          "description": "For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused. Absent for actions without a specific escalation primitive."
+        },
+        "setting": {
+          "type": "string",
+          "description": "The managed setting key responsible for the enforcement (e.g. `permissions.disableBypassPermissionsMode`)."
+        },
+        "failClosed": {
+          "type": "boolean",
+          "description": "Whether the enforcement was forced by fail-closed handling (managed policy could not be determined) rather than an explicit managed setting. When true, `setting` still names the restriction that was applied."
+        },
+        "message": {
+          "type": "string",
+          "description": "A human-readable explanation of why the action was governed, suitable for surfacing to the user."
+        }
+      },
+      "required": [
+        "action",
+        "setting",
+        "failClosed",
+        "message"
+      ],
+      "additionalProperties": false,
+      "description": "Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes.",
+      "title": "ManagedSettingsEnforcedData",
+      "stability": "experimental"
+    },
+    "ManagedSettingsEnforcedEscalation": {
+      "type": "string",
+      "enum": [
+        "allow_all",
+        "approve_all",
+        "auto_approval",
+        "unrestricted_paths",
+        "unrestricted_urls"
+      ],
+      "description": "For a `bypass_permissions_blocked` action, which permission-escalation primitive was refused",
+      "title": "ManagedSettingsEnforcedEscalation",
+      "x-enumDescriptions": {
+        "allow_all": "Full allow-all (\"/allow-all on\") permissions — auto-approving tools, paths, and URLs.",
+        "approve_all": "Auto-approval of all tool permission requests.",
+        "auto_approval": "Advisory auto-approval (\"/allow-all auto\") mode — keeps normal prompt paths and adds LLM-advised approval, distinct from full allow-all.",
+        "unrestricted_paths": "Unrestricted filesystem access outside the session's allowed directories.",
+        "unrestricted_urls": "Unrestricted URL fetch access."
+      }
+    },
+    "ManagedSettingsEnforcedEvent": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid",
+          "description": "Unique event identifier (UUID v4), generated when the event is emitted"
+        },
+        "timestamp": {
+          "type": "string",
+          "format": "date-time",
+          "description": "ISO 8601 timestamp when the event was created"
+        },
+        "parentId": {
+          "anyOf": [
+            {
+              "type": "string",
+              "format": "uuid"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "description": "ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event."
+        },
+        "ephemeral": {
+          "type": "boolean",
+          "const": true,
+          "description": "Always true for events that are transient and not persisted to the session event log on disk."
+        },
+        "agentId": {
+          "type": "string",
+          "description": "Sub-agent instance identifier. Absent for events from the root/main agent and session-level events."
+        },
+        "type": {
+          "type": "string",
+          "const": "session.managed_settings_enforced",
+          "description": "Type discriminator. Always \"session.managed_settings_enforced\"."
+        },
+        "data": {
+          "$ref": "#/definitions/ManagedSettingsEnforcedData",
+          "description": "Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes."
+        }
+      },
+      "required": [
+        "id",
+        "timestamp",
+        "parentId",
+        "ephemeral",
+        "type",
+        "data"
+      ],
+      "additionalProperties": false,
+      "description": "Session event \"session.managed_settings_enforced\". Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes.",
+      "title": "ManagedSettingsEnforcedEvent",
+      "stability": "experimental"
+    },
+    "ManagedSettingsResolvedData": {
+      "type": "object",
+      "properties": {
+        "source": {
+          "$ref": "#/definitions/ManagedSettingsResolvedSource",
+          "description": "Which channel supplied the effective managed settings (the winning layer), or `none` when no policy is in force"
+        },
+        "serverManaged": {
+          "type": "boolean",
+          "description": "Whether the server (account/org) managed-settings layer was present"
+        },
+        "deviceManaged": {
+          "type": "boolean",
+          "description": "Whether the device (MDM/plist/registry/file) managed-settings layer was present"
         },
-        "url": {
-          "type": "string",
-          "description": "Optional URL associated with this message that the user can open in a browser"
+        "failClosed": {
+          "type": "boolean",
+          "description": "Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent."
         },
-        "tip": {
-          "type": "string",
-          "description": "Optional actionable tip displayed with this message"
+        "bypassPermissionsDisabled": {
+          "type": "boolean",
+          "description": "Whether enterprise policy disables bypass-permissions (\"yolo\") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true."
+        },
+        "managedKeys": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "description": "The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force."
+        },
+        "settings": {
+          "description": "The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force.",
+          "x-opaque-json": true
         }
       },
       "required": [
-        "infoType",
-        "message"
+        "source",
+        "serverManaged",
+        "deviceManaged",
+        "failClosed",
+        "bypassPermissionsDisabled",
+        "managedKeys"
       ],
       "additionalProperties": false,
-      "description": "Informational message for timeline display with categorization",
-      "title": "InfoData"
+      "description": "Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes.",
+      "title": "ManagedSettingsResolvedData",
+      "stability": "experimental"
     },
-    "InfoEvent": {
+    "ManagedSettingsResolvedEvent": {
       "type": "object",
       "properties": {
         "id": {
@@ -6289,7 +6699,8 @@
         },
         "ephemeral": {
           "type": "boolean",
-          "description": "When true, the event is transient and not persisted to the session event log on disk"
+          "const": true,
+          "description": "Always true for events that are transient and not persisted to the session event log on disk."
         },
         "agentId": {
           "type": "string",
@@ -6297,24 +6708,41 @@
         },
         "type": {
           "type": "string",
-          "const": "session.info",
-          "description": "Type discriminator. Always \"session.info\"."
+          "const": "session.managed_settings_resolved",
+          "description": "Type discriminator. Always \"session.managed_settings_resolved\"."
         },
         "data": {
-          "$ref": "#/definitions/InfoData",
-          "description": "Informational message for timeline display with categorization"
+          "$ref": "#/definitions/ManagedSettingsResolvedData",
+          "description": "Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes."
         }
       },
       "required": [
         "id",
         "timestamp",
         "parentId",
+        "ephemeral",
         "type",
         "data"
       ],
       "additionalProperties": false,
-      "description": "Session event \"session.info\". Informational message for timeline display with categorization",
-      "title": "InfoEvent"
+      "description": "Session event \"session.managed_settings_resolved\". Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes.",
+      "title": "ManagedSettingsResolvedEvent",
+      "stability": "experimental"
+    },
+    "ManagedSettingsResolvedSource": {
+      "type": "string",
+      "enum": [
+        "server",
+        "device",
+        "none"
+      ],
+      "description": "Which channel supplied the effective enterprise managed settings (highest-authority present layer wins wholesale)",
+      "title": "ManagedSettingsResolvedSource",
+      "x-enumDescriptions": {
+        "server": "Account/org policy self-fetched from the GitHub managed-settings endpoint (higher authority).",
+        "device": "Device-level MDM policy discovered from plist/registry/file (lower authority).",
+        "none": "No managed policy is in force (no layer contributed)."
+      }
     },
     "McpAppToolCallCompleteData": {
       "type": "object",
@@ -6672,7 +7100,7 @@
         "serverName"
       ],
       "additionalProperties": false,
-      "description": "Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed.",
+      "description": "Payload identifying the MCP server associated with a list change.",
       "title": "McpListChangedData"
     },
     "McpOauthCompletedData": {
@@ -6764,6 +7192,36 @@
         "cancelled": "The request completed without an OAuth provider."
       }
     },
+    "McpOauthHttpResponse": {
+      "type": "object",
+      "properties": {
+        "statusCode": {
+          "type": "integer",
+          "minimum": 100,
+          "maximum": 999,
+          "description": "HTTP status code returned with the auth challenge."
+        },
+        "headers": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/HeaderEntry",
+            "description": "Single HTTP header entry as a name/value pair."
+          },
+          "description": "HTTP response headers as observed by the runtime. Order and casing are transport-dependent, and duplicate header names may appear multiple times."
+        },
+        "body": {
+          "type": "string",
+          "description": "Complete UTF-8 response body for host-specific challenge handling, including an empty string for an empty body. Omitted when the complete body is not valid UTF-8; body read failures fail the HTTP operation rather than exposing a partial response."
+        }
+      },
+      "required": [
+        "statusCode",
+        "headers"
+      ],
+      "additionalProperties": false,
+      "description": "Raw HTTP response details from the OAuth auth challenge, as observed by the runtime.",
+      "title": "McpOauthHttpResponse"
+    },
     "McpOauthRequestReason": {
       "type": "string",
       "enum": [
@@ -6804,6 +7262,10 @@
           "$ref": "#/definitions/McpOauthWWWAuthenticateParams",
           "description": "OAuth WWW-Authenticate parameters parsed from the auth challenge, if available"
         },
+        "httpResponse": {
+          "$ref": "#/definitions/McpOauthHttpResponse",
+          "description": "Raw HTTP response details from the OAuth auth challenge, as observed by the runtime. Header order and casing are transport-dependent, and duplicate header names may appear multiple times."
+        },
         "resourceMetadata": {
           "type": "string",
           "description": "Raw OAuth protected-resource metadata document fetched for the MCP server, if available"
@@ -6968,7 +7430,7 @@
         },
         "data": {
           "$ref": "#/definitions/McpListChangedData",
-          "description": "Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed."
+          "description": "Payload identifying the MCP server associated with a list change."
         }
       },
       "required": [
@@ -6980,7 +7442,7 @@
         "data"
       ],
       "additionalProperties": false,
-      "description": "Session event \"mcp.prompts.list_changed\". Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed.",
+      "description": "Session event \"mcp.prompts.list_changed\". Payload identifying the MCP server associated with a list change.",
       "title": "McpPromptsListChangedEvent"
     },
     "McpResourcesListChangedEvent": {
@@ -7024,7 +7486,7 @@
         },
         "data": {
           "$ref": "#/definitions/McpListChangedData",
-          "description": "Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed."
+          "description": "Payload identifying the MCP server associated with a list change."
         }
       },
       "required": [
@@ -7036,7 +7498,7 @@
         "data"
       ],
       "additionalProperties": false,
-      "description": "Session event \"mcp.resources.list_changed\". Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed.",
+      "description": "Session event \"mcp.resources.list_changed\". Payload identifying the MCP server associated with a list change.",
       "title": "McpResourcesListChangedEvent"
     },
     "McpServersLoadedData": {
@@ -7330,7 +7792,7 @@
         },
         "data": {
           "$ref": "#/definitions/McpListChangedData",
-          "description": "Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed."
+          "description": "Payload identifying the MCP server associated with a list change."
         }
       },
       "required": [
@@ -7342,7 +7804,7 @@
         "data"
       ],
       "additionalProperties": false,
-      "description": "Session event \"mcp.tools.list_changed\". Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed.",
+      "description": "Session event \"mcp.tools.list_changed\". Payload identifying the MCP server associated with a list change.",
       "title": "McpToolsListChangedEvent"
     },
     "MemoryChangedData": {
@@ -7532,6 +7994,40 @@
           "format": "duration",
           "description": "Duration of the failed API call in milliseconds"
         },
+        "apiEndpoint": {
+          "$ref": "#/definitions/AssistantUsageApiEndpoint",
+          "description": "API endpoint used for this model call, matching CAPI supported_endpoints vocabulary"
+        },
+        "transport": {
+          "$ref": "#/definitions/ModelCallFailureTransport",
+          "description": "Transport used for the failed model call (http or websocket)"
+        },
+        "failureKind": {
+          "$ref": "#/definitions/ModelCallFailureKind",
+          "description": "Whether the failure originated from an API response or the request transport"
+        },
+        "maxPromptTokens": {
+          "type": "integer",
+          "minimum": 0,
+          "description": "Effective maximum prompt-token limit for the failed call"
+        },
+        "maxOutputTokens": {
+          "type": "integer",
+          "minimum": 0,
+          "description": "Effective maximum output-token limit for the failed call"
+        },
+        "isByok": {
+          "type": "boolean",
+          "description": "Whether the failed call used a bring-your-own-key provider"
+        },
+        "isAuto": {
+          "type": "boolean",
+          "description": "Whether the session selected Auto mode for the failed call"
+        },
+        "reasoningEffort": {
+          "type": "string",
+          "description": "Reasoning effort level used for the failed model call, if applicable"
+        },
         "source": {
           "$ref": "#/definitions/ModelCallFailureSource",
           "description": "Where the failed model call originated"
@@ -7630,6 +8126,19 @@
       "description": "Session event \"model.call_failure\". Failed LLM API call metadata for telemetry",
       "title": "ModelCallFailureEvent"
     },
+    "ModelCallFailureKind": {
+      "type": "string",
+      "enum": [
+        "api",
+        "transport"
+      ],
+      "description": "Boundary that produced a model call failure",
+      "title": "ModelCallFailureKind",
+      "x-enumDescriptions": {
+        "api": "The provider returned an API error response.",
+        "transport": "The request transport failed before a usable API response completed."
+      }
+    },
     "ModelCallFailureRequestFingerprint": {
       "type": "object",
       "properties": {
@@ -7695,6 +8204,95 @@
         "mcp_sampling": "Model call from MCP sampling."
       }
     },
+    "ModelCallFailureTransport": {
+      "type": "string",
+      "enum": [
+        "http",
+        "websocket"
+      ],
+      "description": "Transport used for a failed model call",
+      "title": "ModelCallFailureTransport",
+      "x-enumDescriptions": {
+        "http": "HTTP transport, including SSE streams.",
+        "websocket": "WebSocket transport."
+      }
+    },
+    "ModelCallStartData": {
+      "type": "object",
+      "properties": {
+        "turnId": {
+          "type": "string",
+          "description": "Identifier of the assistant turn that initiated the model call"
+        },
+        "model": {
+          "type": "string",
+          "description": "Model identifier used for this API call, when known"
+        }
+      },
+      "required": [
+        "turnId"
+      ],
+      "additionalProperties": false,
+      "description": "Model API dispatch metadata for internal telemetry",
+      "title": "ModelCallStartData"
+    },
+    "ModelCallStartEvent": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid",
+          "description": "Unique event identifier (UUID v4), generated when the event is emitted"
+        },
+        "timestamp": {
+          "type": "string",
+          "format": "date-time",
+          "description": "ISO 8601 timestamp when the event was created"
+        },
+        "parentId": {
+          "anyOf": [
+            {
+              "type": "string",
+              "format": "uuid"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "description": "ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event."
+        },
+        "ephemeral": {
+          "type": "boolean",
+          "const": true,
+          "description": "Always true for events that are transient and not persisted to the session event log on disk."
+        },
+        "agentId": {
+          "type": "string",
+          "description": "Sub-agent instance identifier. Absent for events from the root/main agent and session-level events."
+        },
+        "type": {
+          "type": "string",
+          "const": "model.call_start",
+          "description": "Type discriminator. Always \"model.call_start\"."
+        },
+        "data": {
+          "$ref": "#/definitions/ModelCallStartData",
+          "description": "Model API dispatch metadata for internal telemetry"
+        }
+      },
+      "required": [
+        "id",
+        "timestamp",
+        "parentId",
+        "ephemeral",
+        "type",
+        "data"
+      ],
+      "additionalProperties": false,
+      "description": "Session event \"model.call_start\". Model API dispatch metadata for internal telemetry",
+      "title": "ModelCallStartEvent",
+      "visibility": "internal"
+    },
     "ModelChangeData": {
       "type": "object",
       "properties": {
@@ -10457,10 +11055,19 @@
           "$ref": "#/definitions/AssistantTurnStartEvent",
           "description": "Session event \"assistant.turn_start\". Turn initialization metadata including identifier and interaction tracking"
         },
+        {
+          "$ref": "#/definitions/AssistantTurnRetryEvent",
+          "description": "Session event \"assistant.turn_retry\". Metadata for an additional model inference attempt within an existing assistant turn",
+          "visibility": "internal"
+        },
         {
           "$ref": "#/definitions/AssistantIntentEvent",
           "description": "Session event \"assistant.intent\". Agent intent description for current activity or plan"
         },
+        {
+          "$ref": "#/definitions/AssistantServerToolProgressEvent",
+          "description": "Session event \"assistant.server_tool_progress\". Live progress signal for a provider-hosted server tool (e.g. hosted web search) while it runs, before the finalized serverTools envelope lands on the terminal assistant.message"
+        },
         {
           "$ref": "#/definitions/AssistantReasoningEvent",
           "description": "Session event \"assistant.reasoning\". Assistant reasoning content for timeline display with complete thinking text"
@@ -10505,6 +11112,11 @@
           "$ref": "#/definitions/ModelCallFailureEvent",
           "description": "Session event \"model.call_failure\". Failed LLM API call metadata for telemetry"
         },
+        {
+          "$ref": "#/definitions/ModelCallStartEvent",
+          "description": "Session event \"model.call_start\". Model API dispatch metadata for internal telemetry",
+          "visibility": "internal"
+        },
         {
           "$ref": "#/definitions/AbortEvent",
           "description": "Session event \"abort\". Turn abort information including the reason for termination"
@@ -10529,6 +11141,10 @@
           "$ref": "#/definitions/ToolExecutionCompleteEvent",
           "description": "Session event \"tool.execution_complete\". Tool execution completion results including success status, detailed output, and error information"
         },
+        {
+          "$ref": "#/definitions/ToolSearchActivatedEvent",
+          "description": "Session event \"tool_search.activated\". Persisted generic client-side tool activations restored when a session resumes."
+        },
         {
           "$ref": "#/definitions/SkillInvokedEvent",
           "description": "Session event \"skill.invoked\". Skill invocation details including content, allowed tools, and plugin metadata"
@@ -10669,6 +11285,14 @@
           "$ref": "#/definitions/AutoModeResolvedEvent",
           "description": "Session event \"session.auto_mode_resolved\". Auto Intent resolution: the concrete model the session settled on for the first prompt of an auto-mode session, and why. Lets SDK clients render the chosen model and the full reason it was picked. The core selection fields (chosenModel/reasoningBucket/categoryScores) are stable; the routing-analytics fields (predictedLabel/confidence/candidateModels) mirror the upstream intent service and may evolve, hence the event's experimental stability."
         },
+        {
+          "$ref": "#/definitions/ManagedSettingsResolvedEvent",
+          "description": "Session event \"session.managed_settings_resolved\". Enterprise managed-settings resolution: the effective managed settings the session applied and where they came from, so SDK clients can show users what is enterprise-managed and by which authority. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted; for a session-independent pull, use the SDK `getManagedSettings()` API, which returns the identical payload. Managed settings have a single authoritative source, so the highest-authority present layer (server > device) wins wholesale; `bypassPermissionsDisabled` is deny-wins across layers. Marked experimental while the managed-settings surface stabilizes."
+        },
+        {
+          "$ref": "#/definitions/ManagedSettingsEnforcedEvent",
+          "description": "Session event \"session.managed_settings_enforced\". Runtime enforcement of enterprise managed settings: fires when the session blocks or caps a runtime action because enterprise policy governs it, so SDK clients can explain *why* an action was governed. Unlike `session.managed_settings_resolved` (which reports *what* is managed), this reports a concrete governed action — e.g. a user or host tried to turn on a bypass-permissions escalation while policy disables it. Emitted live (not persisted to the session event log) on user/host-initiated attempts only, never for silent policy application. Marked experimental while the managed-settings surface stabilizes."
+        },
         {
           "$ref": "#/definitions/CommandsChangedEvent",
           "description": "Session event \"commands.changed\". SDK command registration change notification"
@@ -10711,15 +11335,15 @@
         },
         {
           "$ref": "#/definitions/McpToolsListChangedEvent",
-          "description": "Session event \"mcp.tools.list_changed\". Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed."
+          "description": "Session event \"mcp.tools.list_changed\". Payload identifying the MCP server associated with a list change."
         },
         {
           "$ref": "#/definitions/McpResourcesListChangedEvent",
-          "description": "Session event \"mcp.resources.list_changed\". Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed."
+          "description": "Session event \"mcp.resources.list_changed\". Payload identifying the MCP server associated with a list change."
         },
         {
           "$ref": "#/definitions/McpPromptsListChangedEvent",
-          "description": "Session event \"mcp.prompts.list_changed\". Payload of MCP `list_changed` notification events, emitted when an MCP server announces at runtime that one of its advertised lists changed."
+          "description": "Session event \"mcp.prompts.list_changed\". Payload identifying the MCP server associated with a list change."
         },
         {
           "$ref": "#/definitions/ExtensionsLoadedEvent",
@@ -14006,6 +14630,83 @@
         "app": "Tool is callable by the MCP App view (iframe) via session.mcp.apps.callTool"
       }
     },
+    "ToolSearchActivatedData": {
+      "type": "object",
+      "properties": {
+        "strategy": {
+          "type": "string",
+          "description": "Tool-search strategy that activated the definitions."
+        },
+        "toolNames": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "description": "Names of tool definitions activated by this search invocation."
+        }
+      },
+      "required": [
+        "strategy",
+        "toolNames"
+      ],
+      "additionalProperties": false,
+      "description": "Persisted generic client-side tool activations restored when a session resumes.",
+      "title": "ToolSearchActivatedData"
+    },
+    "ToolSearchActivatedEvent": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string",
+          "format": "uuid",
+          "description": "Unique event identifier (UUID v4), generated when the event is emitted"
+        },
+        "timestamp": {
+          "type": "string",
+          "format": "date-time",
+          "description": "ISO 8601 timestamp when the event was created"
+        },
+        "parentId": {
+          "anyOf": [
+            {
+              "type": "string",
+              "format": "uuid"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "description": "ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event."
+        },
+        "ephemeral": {
+          "type": "boolean",
+          "description": "When true, the event is transient and not persisted to the session event log on disk"
+        },
+        "agentId": {
+          "type": "string",
+          "description": "Sub-agent instance identifier. Absent for events from the root/main agent and session-level events."
+        },
+        "type": {
+          "type": "string",
+          "const": "tool_search.activated",
+          "description": "Type discriminator. Always \"tool_search.activated\"."
+        },
+        "data": {
+          "$ref": "#/definitions/ToolSearchActivatedData",
+          "description": "Persisted generic client-side tool activations restored when a session resumes."
+        }
+      },
+      "required": [
+        "id",
+        "timestamp",
+        "parentId",
+        "type",
+        "data"
+      ],
+      "additionalProperties": false,
+      "description": "Session event \"tool_search.activated\". Persisted generic client-side tool activations restored when a session resumes.",
+      "title": "ToolSearchActivatedEvent"
+    },
     "ToolsUpdatedData": {
       "type": "object",
       "properties": {
@@ -14279,6 +14980,16 @@
           "minimum": 0,
           "description": "Total number of premium API requests used at checkpoint time",
           "visibility": "internal"
+        },
+        "modelCacheState": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/UsageCheckpointModelCacheState",
+            "description": "Internal prompt-cache expiration state for one model",
+            "visibility": "internal"
+          },
+          "description": "Internal per-model prompt-cache state used to restore expiration tracking on resume",
+          "visibility": "internal"
         }
       },
       "required": [
@@ -14342,6 +15053,35 @@
       "description": "Session event \"session.usage_checkpoint\". Durable session usage checkpoint for reconstructing aggregate accounting on resume",
       "title": "UsageCheckpointEvent"
     },
+    "UsageCheckpointModelCacheState": {
+      "type": "object",
+      "properties": {
+        "modelId": {
+          "type": "string",
+          "description": "Model identifier associated with this cache state"
+        },
+        "cacheExpiresAt": {
+          "type": "string",
+          "format": "date-time",
+          "description": "Latest known prompt-cache expiration"
+        },
+        "cacheTtlSeconds": {
+          "type": "integer",
+          "minimum": 0,
+          "description": "Retained cache lifetime in seconds, used to refresh expiration after a cache read",
+          "visibility": "internal"
+        }
+      },
+      "required": [
+        "modelId",
+        "cacheExpiresAt",
+        "cacheTtlSeconds"
+      ],
+      "additionalProperties": false,
+      "description": "Internal prompt-cache expiration state for one model",
+      "title": "UsageCheckpointModelCacheState",
+      "visibility": "internal"
+    },
     "UsageInfoData": {
       "type": "object",
       "properties": {
diff --git a/script/codegen/emit_specs.clj b/script/codegen/emit_specs.clj
index 89d7093a..1dcb9fc1 100644
--- a/script/codegen/emit_specs.clj
+++ b/script/codegen/emit_specs.clj
@@ -93,7 +93,7 @@
 ;;   - Walk every variant's envelope properties + data.properties.
 ;;   - For each unique kebab property name, collect all observed schema nodes.
 ;;   - If they all yield the same emitted spec form → use it.
-;;   - Otherwise → fall back to `any?` (and warn on stderr).
+;;   - Otherwise → emit a non-conforming union and add variant-local predicates.
 
 (defn- walk-props
   "Collect [kebab-name schema-node] tuples from an object schema's properties."
@@ -134,18 +134,13 @@
                          emitting a redundant union-of-all-variants strict
                          predicate would noticeably bloat validation.
 
-   `:data-form-by-kebab` — kebab-name → strict spec form derived from
-                         data-payload occurrences only. The data-side
-                         counterpart of `:env-form-by-kebab`.
-
-   `:data-conflicted`  — set of kebab-names whose `:leaf-map` form is a
-                         non-conforming union *and* the envelope side
-                         contributes a form not already present on the data
-                         side. Data emission adds a per-property strict
-                         predicate for every data key in this set, so e.g.
-                         `session.schedule_*-data` `:id` is validated as a
-                         positive integer rather than the `string? OR
-                         integer?` union."
+   `:data-conflicted`  — set of kebab-names whose global `:leaf-map` form is
+                         a non-conforming union and which occur in at least
+                         one data payload. Data emission adds a variant-local
+                         strict predicate for every data key in this set, so
+                         both envelope/data collisions (`id`) and collisions
+                         across data variants (`reason`) retain the exact
+                         schema declared by each event."
   [root variants]
   (let [env-pairs  (mapcat (fn [{:keys [variant]}]
                              (walk-props (:properties variant)))
@@ -174,11 +169,9 @@
                          (~'fn [~v]
                           (~'or ~@(map (fn [f] `(~'s/valid? ~f ~v)) forms))))))])))
         env-form-by-kebab  (side-form env-groups)
-        data-form-by-kebab (side-form data-groups)
         ;; Per-kebab distinct envelope/data forms — used to decide whether a
-        ;; key's leaf-union is *truly* weakened from the envelope or data
-        ;; side's perspective (i.e., the other side contributes a form not
-        ;; already present on this side).
+        ;; key's leaf-union is *truly* weakened from the envelope side (i.e.,
+        ;; the data side contributes a form not already present there).
         env-forms-by-kebab
         (into {} (for [[k pairs] env-groups]
                    [k (set (map #(emit-type root (second %)) pairs))]))
@@ -205,20 +198,18 @@
                         ;; side contributes a form not already present here.
                         env-weakened?  (and (seq env-fs)
                                             (some #(not (contains? env-fs %)) data-fs))
-                        data-weakened? (and (seq data-fs)
-                                            (some #(not (contains? data-fs %)) env-fs))]
+                        data-conflict? (seq data-fs)]
                     (when env-weakened?  (swap! env-conflicted conj kebab))
-                    (when data-weakened? (swap! data-conflicted conj kebab))
+                    (when data-conflict? (swap! data-conflicted conj kebab))
                     (binding [*out* *err*]
                       (println (format "INFO: property '%s' has %d distinct schemas — emitting non-conforming union%s%s"
                                        kebab (count uniq)
                                        (if env-weakened?  " (envelope strict-pred added)" "")
-                                       (if data-weakened? " (data strict-pred added)"     ""))))
+                                       (if data-conflict? " (data strict-pred added)"     ""))))
                     [kebab union-form]))))]
     {:leaf-map           leaf-map
      :env-form-by-kebab  env-form-by-kebab
      :conflicted         @env-conflicted
-     :data-form-by-kebab data-form-by-kebab
      :data-conflicted    @data-conflicted}))
 
 ;; ---------------------------------------------------------------------------
@@ -235,16 +226,15 @@
 (defn- emit-data-spec
   "Emit `(s/def ::-data ...)` for one event's data payload.
 
-   When a data property's name conflicts with an envelope property (different
-   schema), the global leaf spec is a non-conforming union — see
-   `collect-leaf-properties`. Without intervention the data `s/keys` would
-   accept the weakened union (e.g. `session.schedule_*-data` `:id` would
-   accept a UUID string even though the schema requires a positive integer).
-   For each data key in `data-conflicted`, emit an extra predicate
-   validating against the strict data-only form (`data-form-by-kebab`).
+   When a data property's name has different schemas elsewhere, the global
+   leaf spec is a non-conforming union — see `collect-leaf-properties`.
+   Without intervention the data `s/keys` would accept the weakened union
+   (e.g. abort `:reason` would accept any string after another event adds an
+   open string-valued reason). For each data key in `data-conflicted`, emit an
+   extra predicate validating against that event variant's property schema.
    Required keys are validated unconditionally; optional keys only when
    present."
-  [root variant data-form-by-kebab data-conflicted]
+  [root variant data-conflicted]
   (let [event-type (get-in variant [:properties :type :const])
         data-node  (cc/deref-once root (get-in variant [:properties :data]))
         props      (:properties data-node)
@@ -266,10 +256,10 @@
                      (seq opt-keys) (concat [:opt-un opt-keys])
                      true           seq)
         strict-preds (->> props
-                          (keep (fn [[k _]]
+                          (keep (fn [[k node]]
                                   (let [kb        (kebab k)
-                                        data-form (get data-form-by-kebab kb)]
-                                    (when (and (contains? data-conflicted kb) data-form)
+                                        data-form (emit-type root node)]
+                                    (when (contains? data-conflicted kb)
                                       [kb data-form (contains? required (name k))]))))
                           (sort-by first)
                           (map (fn [[prop-name data-form req?]]
@@ -393,8 +383,7 @@
   (let [variants  (cc/collect-anyOf-discriminators root)
         ;; Sort variants by event-type for deterministic emission.
         sorted    (sort-by :type variants)
-        {:keys [leaf-map env-form-by-kebab conflicted
-                data-form-by-kebab data-conflicted]}
+        {:keys [leaf-map env-form-by-kebab conflicted data-conflicted]}
         (collect-leaf-properties root variants)]
     (concat
       [`(~'ns ~(symbol ns-name)
@@ -408,7 +397,7 @@
    Source: schemas/session-events.schema.json"
               (:require [clojure.spec.alpha :as ~'s]))]
       (emit-leaf-defs leaf-map)
-      (mapv #(emit-data-spec root (:variant %) data-form-by-kebab data-conflicted) sorted)
+      (mapv #(emit-data-spec root (:variant %) data-conflicted) sorted)
       (mapv #(emit-envelope-spec (:variant %) env-form-by-kebab conflicted) sorted)
       [(emit-event-types-set variants)]
       (emit-event-multi-spec variants))))
diff --git a/src/github/copilot_sdk.clj b/src/github/copilot_sdk.clj
index 2a5ac5f3..2106b8da 100644
--- a/src/github/copilot_sdk.clj
+++ b/src/github/copilot_sdk.clj
@@ -174,7 +174,13 @@
     :copilot/mcp.tools.list_changed
     :copilot/mcp.resources.list_changed
     :copilot/mcp.prompts.list_changed
-    :copilot/session.auto_mode_resolved})
+    :copilot/session.auto_mode_resolved
+    ;; Post-v1.0.7 sync (pinned schema 1.0.73). assistant.turn_retry and
+    ;; model.call_start are generated for wire compatibility but stay internal.
+    :copilot/assistant.server_tool_progress
+    :copilot/session.managed_settings_enforced
+    :copilot/session.managed_settings_resolved
+    :copilot/tool_search.activated})
 
 (def session-events
   "Session lifecycle and state management events."
@@ -222,7 +228,11 @@
     :copilot/session.usage_checkpoint
     ;; v1.0.7-preview.2 sync (pinned schema 1.0.70): experimental auto-mode
     ;; model resolution for the first prompt of an auto-mode session.
-    :copilot/session.auto_mode_resolved})
+    :copilot/session.auto_mode_resolved
+    ;; Post-v1.0.7 sync (pinned schema 1.0.73): enterprise managed-settings
+    ;; resolution and enforcement.
+    :copilot/session.managed_settings_enforced
+    :copilot/session.managed_settings_resolved})
 
 (def assistant-events
   "Assistant response events."
@@ -239,7 +249,9 @@
     ;; v1.0.5-preview.0 sync (pinned schema 1.0.66-2): assistant idle within a turn.
     :copilot/assistant.idle
     ;; v1.0.7-preview.2 sync (introduced upstream schema 1.0.69-3): streaming tool-call input delta.
-    :copilot/assistant.tool_call_delta})
+    :copilot/assistant.tool_call_delta
+    ;; Post-v1.0.7 sync (pinned schema 1.0.73): server-side tool progress.
+    :copilot/assistant.server_tool_progress})
 
 (def tool-events
   "Tool execution events."
diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj
index 981be1f2..ddbb2e30 100644
--- a/src/github/copilot_sdk/client.clj
+++ b/src/github/copilot_sdk/client.clj
@@ -915,7 +915,7 @@
                                       "hooks.invoke"
                                       (let [{:keys [session-id hook-type input]} params]
                                         (if-not (get-in @(:state client) [:sessions session-id])
-                                          {:result nil}
+                                          {:error {:code -32001 :message (str "Unknown session: " session-id)}}
                                           ( {}
+                            (some? result) (assoc :output result))})
                (catch Exception e
                  (log/error "Hook handler error for session " session-id ", hook " hook-type ": " (ex-message e))
-                 {:result nil})))))))
+                 {:result {}})))))))
    :io))
 
 (defn handle-command-execute!
diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj
index 18d4aebc..b9f6fd5b 100644
--- a/src/github/copilot_sdk/specs.clj
+++ b/src/github/copilot_sdk/specs.clj
@@ -571,10 +571,42 @@
 (s/def ::providers (s/coll-of ::named-provider))
 (s/def ::models (s/coll-of ::provider-model))
 
-;; expAssignments (upstream PR #1750, @internal) — opaque experiment flight
-;; assignments. Keys are source-defined flight ids; forwarded verbatim, so
-;; they must be strings (bypassing kebab->camel key conversion).
-(s/def ::exp-assignments (s/map-of string? any?))
+;; expAssignments (upstream PR #2033, @internal) uses the PascalCase JSON shape
+;; returned by ExP. String keys bypass kebab->camel conversion and are forwarded
+;; verbatim to the runtime.
+(s/def ::exp-flag-value
+  (s/or :string string?
+        :number number?
+        :boolean boolean?
+        :nil nil?))
+(s/def ::exp-parameters (s/map-of string? ::exp-flag-value))
+
+(def ^:private exp-config-entry-keys #{"Id" "Parameters"})
+
+(s/def ::exp-config-entry
+  (s/and map?
+         #(= exp-config-entry-keys (set (keys %)))
+         #(string? (get % "Id"))
+         #(s/valid? ::exp-parameters (get % "Parameters"))))
+
+(def ^:private exp-assignment-required-keys
+  #{"Features" "Flights" "Configs" "AssignmentContext"})
+(def ^:private exp-assignment-keys
+  (into exp-assignment-required-keys
+        ["ParameterGroups" "FlightingVersion" "ImpressionId"]))
+
+(s/def ::exp-assignments
+  (s/and map?
+         #(set/subset? exp-assignment-required-keys (set (keys %)))
+         #(set/subset? (set (keys %)) exp-assignment-keys)
+         #(s/valid? (s/coll-of string?) (get % "Features"))
+         #(s/valid? (s/map-of string? string?) (get % "Flights"))
+         #(s/valid? (s/coll-of ::exp-config-entry) (get % "Configs"))
+         #(string? (get % "AssignmentContext"))
+         #(or (not (contains? % "FlightingVersion"))
+              (number? (get % "FlightingVersion")))
+         #(or (not (contains? % "ImpressionId"))
+              (string? (get % "ImpressionId")))))
 
 ;; -----------------------------------------------------------------------------
 ;; Session configuration
@@ -645,11 +677,12 @@
 (s/def ::on-session-start fn?)
 (s/def ::on-session-end fn?)
 (s/def ::on-error-occurred fn?)
+(s/def ::on-agent-stop fn?)
 (s/def ::hooks
   (s/keys :opt-un [::on-pre-tool-use ::on-pre-mcp-tool-call ::on-post-tool-use
                    ::on-post-tool-use-failure
                    ::on-user-prompt-submitted ::on-session-start ::on-session-end
-                   ::on-error-occurred]))
+                   ::on-error-occurred ::on-agent-stop]))
 
 ;; Disable resume flag
 (s/def ::disable-resume? boolean?)
@@ -1326,7 +1359,13 @@
     :copilot/mcp.tools.list_changed
     :copilot/mcp.resources.list_changed
     :copilot/mcp.prompts.list_changed
-    :copilot/session.auto_mode_resolved})
+    :copilot/session.auto_mode_resolved
+    ;; Post-v1.0.7 schema sync (pinned schema 1.0.73). assistant.turn_retry and
+    ;; model.call_start remain generated-only because upstream marks them internal.
+    :copilot/assistant.server_tool_progress
+    :copilot/session.managed_settings_enforced
+    :copilot/session.managed_settings_resolved
+    :copilot/tool_search.activated})
 
 ;; Session events
 (s/def ::already-in-use? boolean?)
diff --git a/test/github/copilot_sdk/codegen_test.clj b/test/github/copilot_sdk/codegen_test.clj
index 29022a33..3fa1af98 100644
--- a/test/github/copilot_sdk/codegen_test.clj
+++ b/test/github/copilot_sdk/codegen_test.clj
@@ -146,6 +146,14 @@
    "assistant.turn_start"
    {:turn-id "t-1"}
 
+   "assistant.turn_retry"
+   {:turn-id "t-1"}
+
+   "assistant.server_tool_progress"
+   {:kind "web_search"
+    :output-index 0
+    :status "in_progress"}
+
    "assistant.reasoning"
    {:reasoning-id "r-1"
     :content "thinking"}
@@ -177,6 +185,10 @@
    {:tool-call-id "tc-1"
     :success true}
 
+   "tool_search.activated"
+   {:strategy "deferred"
+    :tool-names ["shell"]}
+
    "skill.invoked"
    {:name "my-skill"
     :path "/skills/my-skill"
@@ -212,6 +224,9 @@
    "session.context_changed"
    {:cwd "/tmp"}
 
+   "model.call_start"
+   {:turn-id "t-1"}
+
    "session.mode_changed"
    {:previous-mode "interactive"
     :new-mode "plan"}
@@ -231,6 +246,20 @@
     :warnings []
     :errors []}
 
+   "session.managed_settings_resolved"
+   {:bypass-permissions-disabled false
+    :device-managed false
+    :fail-closed false
+    :managed-keys []
+    :server-managed false
+    :source "none"}
+
+   "session.managed_settings_enforced"
+   {:action "bypass_permissions_blocked"
+    :fail-closed false
+    :message "Bypass permissions mode is disabled"
+    :setting "permissions.disableBypassPermissionsMode"}
+
    "session.mcp_servers_loaded"
    {:servers []}
 
@@ -324,7 +353,7 @@
           (str "public event-types not present in the schema: " (sort extra)
                " — remove them or update the schema pin")))))
 
-(deftest generated-data-specs-reject-envelope-weakened-types
+(deftest generated-data-specs-preserve-variant-local-types
   (testing "session.schedule_created-data rejects string :id (must be positive integer)"
     (let [spec-kw :github.copilot-sdk.generated.event-specs/session.schedule_created-data]
       (is (not (s/valid? spec-kw {:id "uuid-string" :interval-ms 1000 :prompt "x"}))
@@ -332,7 +361,17 @@
   (testing "session.schedule_cancelled-data rejects string :id (must be positive integer)"
     (let [spec-kw :github.copilot-sdk.generated.event-specs/session.schedule_cancelled-data]
       (is (not (s/valid? spec-kw {:id "uuid-string"}))
-          "data spec must not accept envelope-shaped UUID :id"))))
+          "data spec must not accept envelope-shaped UUID :id")))
+  (testing "same-named data properties keep each event variant's schema"
+    (let [abort-spec :github.copilot-sdk.generated.event-specs/abort-data
+          retry-spec :github.copilot-sdk.generated.event-specs/assistant.turn_retry-data]
+      (doseq [reason ["user_initiated" "remote_command" "user_abort"]]
+        (is (s/valid? abort-spec {:reason reason})))
+      (is (not (s/valid? abort-spec {:reason "arbitrary_reason"}))
+          "abort reason must remain a closed enum")
+      (is (s/valid? retry-spec {:turn-id "turn-1"
+                                :reason "arbitrary_reason"})
+          "assistant.turn_retry reason must remain an open string"))))
 
 ;; ---------------------------------------------------------------------------
 ;; Envelope discrimination — type and data binding must be tight.
diff --git a/test/github/copilot_sdk/integration_test.clj b/test/github/copilot_sdk/integration_test.clj
index 926e02a0..b1e540c1 100644
--- a/test/github/copilot_sdk/integration_test.clj
+++ b/test/github/copilot_sdk/integration_test.clj
@@ -2661,6 +2661,42 @@
       (is (s/valid? ::specs/event-type ev)
           (str ev " must be accepted by the idiom ::event-type spec")))))
 
+(deftest test-post-v1-0-7-schema-events
+  (let [generated-events #{"assistant.server_tool_progress"
+                           "assistant.turn_retry"
+                           "model.call_start"
+                           "session.managed_settings_enforced"
+                           "session.managed_settings_resolved"
+                           "tool_search.activated"}
+        public-events #{:copilot/assistant.server_tool_progress
+                        :copilot/session.managed_settings_enforced
+                        :copilot/session.managed_settings_resolved
+                        :copilot/tool_search.activated}
+        internal-events #{:copilot/assistant.turn_retry
+                          :copilot/model.call_start}]
+    (testing "schema 1.0.73 generates all new wire event specs"
+      (doseq [event-type generated-events]
+        (is (contains? github.copilot-sdk.generated.event-specs/event-types event-type)
+            (str event-type " must be generated from the pinned schema"))
+        (is (s/get-spec (keyword "github.copilot-sdk.generated.event-specs"
+                                 (str event-type "-data")))
+            (str event-type " must have a generated data spec"))))
+    (testing "only upstream-public events enter the curated idiom surface"
+      (doseq [event-type public-events]
+        (is (contains? sdk/event-types event-type)
+            (str event-type " must be public"))
+        (is (s/valid? ::specs/event-type event-type)
+            (str event-type " must satisfy the idiom event-type spec")))
+      (doseq [event-type internal-events]
+        (is (not (contains? sdk/event-types event-type))
+            (str event-type " is marked internal upstream"))
+        (is (not (s/valid? ::specs/event-type event-type))
+            (str event-type " must stay outside the public idiom spec"))))
+    (testing "public events are categorized by their SDK domain"
+      (is (contains? sdk/assistant-events :copilot/assistant.server_tool_progress))
+      (is (contains? sdk/session-events :copilot/session.managed_settings_enforced))
+      (is (contains? sdk/session-events :copilot/session.managed_settings_resolved)))))
+
 (deftest test-v1-0-4-provider-transport-wire
   (testing ":provider :transport forwards on both session.create and session.resume (upstream PR #1711)"
     (let [seen (atom {})
@@ -2810,31 +2846,59 @@
                   {:providers [{:name "p" :base-url "https://x.test"}]
                    :models [{:id "m" :provider "p"}]}))))
 
-(deftest test-v1-0-4-exp-assignments-wire
-  (testing ":exp-assignments forwards verbatim on both session.create and session.resume (upstream PR #1750)"
-    (let [seen (atom {})
-          _ (mock/set-request-hook! *mock-server*
-                                    (fn [method params]
-                                      (when (#{"session.create" "session.resume"} method)
-                                        (swap! seen assoc method params))))
-          exp {"flight-abc" "treatment" "feature_x" {"enabled" true}}
-          cfg {:on-permission-request sdk/approve-all
-               :exp-assignments exp}
-          _ (sdk/create-session *test-client* cfg)
-          session-id (sdk/get-last-session-id *test-client*)
-          _ (sdk/resume-session *test-client* session-id cfg)]
-      (doseq [method ["session.create" "session.resume"]]
-        (testing method
-          (let [p (get @seen method)]
-            (is (= {:flight-abc "treatment" :feature_x {:enabled true}}
-                   (:expAssignments p))
-                ":exp-assignments forwards under wire key :expAssignments with keys preserved verbatim (no kebab->camel)"))))))
-  (testing "::exp-assignments accepts an opaque string-keyed map"
-    (is (s/valid? :github.copilot-sdk.specs/exp-assignments {"a" 1 "b" {"c" 2}}))
-    (is (false? (s/valid? :github.copilot-sdk.specs/exp-assignments {:a 1}))
-        "keys must be strings (source-defined flight ids), not keywords")
-    (is (s/valid? :github.copilot-sdk.specs/session-config
-                  {:exp-assignments {"a" 1}}))))
+(deftest test-post-v1-0-7-exp-assignments-wire
+  (let [exp {"Features" ["feature-x"]
+             "Flights" {"flight-abc" "treatment"}
+             "Configs" [{"Id" "config-a"
+                         "Parameters" {"enabled" true
+                                       "threshold" 0.5
+                                       "optional" nil}}]
+             "ParameterGroups" {"group-a" ["config-a"]}
+             "FlightingVersion" 7
+             "ImpressionId" "impression-1"
+             "AssignmentContext" "assignment-context"}]
+    (testing ":exp-assignments forwards its PascalCase contract unchanged on create and resume"
+      (let [seen (atom {})
+            _ (mock/set-request-hook! *mock-server*
+                                      (fn [method params]
+                                        (when (#{"session.create" "session.resume"} method)
+                                          (swap! seen assoc method params))))
+            cfg {:on-permission-request sdk/approve-all
+                 :exp-assignments exp}
+            _ (sdk/create-session *test-client* cfg)
+            session-id (sdk/get-last-session-id *test-client*)
+            _ (sdk/resume-session *test-client* session-id cfg)
+            expected {:Features ["feature-x"]
+                      :Flights {:flight-abc "treatment"}
+                      :Configs [{:Id "config-a"
+                                 :Parameters {:enabled true
+                                              :threshold 0.5
+                                              :optional nil}}]
+                      :ParameterGroups {:group-a ["config-a"]}
+                      :FlightingVersion 7
+                      :ImpressionId "impression-1"
+                      :AssignmentContext "assignment-context"}]
+        (doseq [method ["session.create" "session.resume"]]
+          (testing method
+            (is (= expected (:expAssignments (get @seen method)))
+                "PascalCase field names must bypass kebab-to-camel conversion")))))
+    (testing "::exp-assignments enforces CopilotExpAssignmentResponse (upstream PR #2033)"
+      (is (s/valid? ::specs/exp-assignments exp))
+      (is (s/valid? ::specs/session-config {:exp-assignments exp}))
+      (doseq [required-field ["Features" "Flights" "Configs" "AssignmentContext"]]
+        (is (not (s/valid? ::specs/exp-assignments (dissoc exp required-field)))
+            (str required-field " is required")))
+      (is (not (s/valid? ::specs/exp-assignments
+                         (assoc exp "Configs" [{"Parameters" {}}])))
+          "config Id is required")
+      (is (not (s/valid? ::specs/exp-assignments
+                         (assoc exp "Configs" [{"Id" "config-a"}])))
+          "config Parameters are required")
+      (is (not (s/valid? ::specs/exp-assignments
+                         (assoc-in exp ["Configs" 0 "Parameters" "bad"] [])))
+          "flag values are limited to string, number, boolean, or nil")
+      (is (not (s/valid? ::specs/exp-assignments {"flight-abc" "treatment"}))
+          "the former arbitrary flat-map contract is no longer valid"))))
 
 (deftest test-v1-0-4-provider-and-providers-mutually-exclusive
   (testing "combining singular :provider with the :providers registry is rejected on both create and resume (upstream ProviderTokenArgs/SessionConfig contract, PR #1718)"
@@ -4195,8 +4259,53 @@
       (is (= "bash" (get-in @handler-called [:input :tool-name])))
       (is (= {:command "echo hi"} (get-in @handler-called [:input :tool-args])))
       (is (= session-id (get-in @handler-called [:ctx :session-id])))
-      ;; Response contains the handler's return value (wire-converted)
-      (is (= "allow" (get-in response [:result :permissionDecision]))))))
+      ;; HookInvokeResponse wraps the handler's return value under output.
+      (is (= "allow" (get-in response [:result :output :permissionDecision]))))))
+
+(deftest test-hooks-agent-stop
+  (testing "hooks.invoke agentStop calls the registered handler and returns a block decision"
+    (let [handler-called (atom nil)
+          session (sdk/create-session *test-client*
+                                      {:on-permission-request sdk/approve-all
+                                       :hooks {:on-agent-stop
+                                               (fn [input ctx]
+                                                 (reset! handler-called {:input input :ctx ctx})
+                                                 {:decision "block"
+                                                  :reason "fix the remaining findings"})}})
+          session-id (sdk/session-id session)
+          response (mock/send-rpc-request! *mock-server*
+                                           "hooks.invoke"
+                                           {:sessionId session-id
+                                            :hookType "agentStop"
+                                            :input {:stopReason "end_turn"
+                                                    :transcriptPath "/tmp/transcript.jsonl"
+                                                    :stop_hook_active true
+                                                    :timestamp 1700000000000
+                                                    :cwd "/workspace"}})]
+      (is (s/get-spec ::specs/on-agent-stop))
+      (is (= {:stop-reason "end_turn"
+              :transcript-path "/tmp/transcript.jsonl"
+              :stop-hook-active true
+              :timestamp 1700000000000
+              :cwd "/workspace"
+              :session-id session-id}
+             (:input @handler-called)))
+      (is (= {:session-id session-id} (:ctx @handler-called)))
+      (is (= {:decision "block" :reason "fix the remaining findings"}
+             (get-in response [:result :output])))))
+  (testing "nil and handler errors both let the agent stop"
+    (doseq [handler [(fn [_ _] nil)
+                     (fn [_ _] (throw (Exception. "agent-stop failed")))]]
+      (let [session (sdk/create-session *test-client*
+                                        {:on-permission-request sdk/approve-all
+                                         :hooks {:on-agent-stop handler}})
+            response (mock/send-rpc-request! *mock-server*
+                                             "hooks.invoke"
+                                             {:sessionId (sdk/session-id session)
+                                              :hookType "agentStop"
+                                              :input {:timestamp 1700000000000
+                                                      :cwd "/workspace"}})]
+        (is (= {} (:result response)))))))
 
 (deftest test-hooks-post-tool-use
   (testing "hooks.invoke postToolUse calls registered handler"
@@ -4220,8 +4329,8 @@
                                                     :cwd "/workspace"}})]
       (is (some? @handler-called))
       (is (= "bash" (get-in @handler-called [:input :tool-name])))
-      ;; Handler returned nil, so result is nil
-      (is (nil? (:result response))))))
+      ;; Handler returned nil, so the response has no output.
+      (is (= {} (:result response))))))
 
 (deftest test-hooks-post-tool-use-failure
   (testing "hooks.invoke postToolUseFailure calls registered handler (upstream PR #1421)"
@@ -4246,13 +4355,13 @@
       (is (= "bash" (get-in @handler-called [:input :tool-name])))
       (is (= "command exited 1" (get-in @handler-called [:input :error])))
       (is (= session-id (get-in @handler-called [:input :session-id])))
-      (is (= "noted" (get-in response [:result :additionalContext]))))))
+      (is (= "noted" (get-in response [:result :output :additionalContext]))))))
 
 (deftest test-hooks-post-tool-use-failure-no-handler
-  (testing "hooks.invoke postToolUseFailure with no handler returns nil result"
+  (testing "hooks.invoke postToolUseFailure with no handler returns an empty response"
     (let [session (sdk/create-session *test-client*
                                       {:on-permission-request sdk/approve-all
-                                       ;; Only success hook registered; failure should pass through as nil.
+                                       ;; Only success hook registered; failure should pass through without output.
                                        :hooks {:on-post-tool-use
                                                (fn [_ _] nil)}})
           session-id (sdk/session-id session)
@@ -4265,7 +4374,7 @@
                                                     :error "boom"
                                                     :timestamp 12345
                                                     :cwd "/workspace"}})]
-      (is (nil? (:result response))))))
+      (is (= {} (:result response))))))
 
 (deftest test-hooks-session-start
   (testing "hooks.invoke sessionStart calls registered handler"
@@ -4286,10 +4395,10 @@
                                                     :cwd "/workspace"}})]
       (is (some? @handler-called))
       (is (= "new" (:source @handler-called)))
-      (is (= "welcome" (get-in response [:result :additionalContext]))))))
+      (is (= "welcome" (get-in response [:result :output :additionalContext]))))))
 
-(deftest test-hooks-unknown-type-returns-nil
-  (testing "hooks.invoke with unknown hook type returns nil result"
+(deftest test-hooks-unknown-type-returns-empty-response
+  (testing "hooks.invoke with unknown hook type returns an empty response"
     (let [session (sdk/create-session *test-client*
                                       {:on-permission-request sdk/approve-all
                                        :hooks {:on-pre-tool-use (fn [_ _] {:permission-decision "allow"})}})
@@ -4300,10 +4409,10 @@
                                             :hookType "unknownHookType"
                                             :input {:timestamp 12345
                                                     :cwd "/workspace"}})]
-      (is (nil? (:result response))))))
+      (is (= {} (:result response))))))
 
-(deftest test-hooks-handler-exception-returns-nil
-  (testing "hooks.invoke handler exception returns nil gracefully"
+(deftest test-hooks-handler-exception-returns-empty-response
+  (testing "hooks.invoke handler exception returns an empty response"
     (let [session (sdk/create-session *test-client*
                                       {:on-permission-request sdk/approve-all
                                        :hooks {:on-pre-tool-use (fn [_ _] (throw (Exception. "oops")))}})
@@ -4316,10 +4425,10 @@
                                                     :toolArgs {}
                                                     :timestamp 12345
                                                     :cwd "/workspace"}})]
-      (is (nil? (:result response))))))
+      (is (= {} (:result response))))))
 
 (deftest test-hooks-no-hooks-registered
-  (testing "hooks.invoke with no hooks registered returns nil"
+  (testing "hooks.invoke with no hooks registered returns an empty response"
     (let [session (sdk/create-session *test-client*
                                       {:on-permission-request sdk/approve-all})
           session-id (sdk/session-id session)
@@ -4331,7 +4440,19 @@
                                                     :toolArgs {}
                                                     :timestamp 12345
                                                     :cwd "/workspace"}})]
-      (is (nil? (:result response))))))
+      (is (= {} (:result response))))))
+
+(deftest test-hooks-unknown-session
+  (testing "hooks.invoke with an unknown session returns an RPC error"
+    (is (thrown-with-msg?
+         clojure.lang.ExceptionInfo
+         #"Unknown session: missing-session"
+         (mock/send-rpc-request! *mock-server*
+                                 "hooks.invoke"
+                                 {:sessionId "missing-session"
+                                  :hookType "agentStop"
+                                  :input {:timestamp 1700000000000
+                                          :cwd "/workspace"}})))))
 
 (deftest test-hooks-input-exposes-session-id
   (testing "hook input includes :session-id (upstream PR #1290 — BaseHookInput.sessionId)"
@@ -4459,10 +4580,10 @@
                                                     :cwd "/workspace"
                                                     :sessionId session-id}})]
       ;; The wire field name is metaToUse, NOT meta-to-use
-      (is (contains? (:result response) :metaToUse))
-      (is (not (contains? (:result response) :meta-to-use)))
+      (is (contains? (get-in response [:result :output]) :metaToUse))
+      (is (not (contains? (get-in response [:result :output]) :meta-to-use)))
       ;; Inner map preserved verbatim — inner keys NOT camelCased
-      (is (= opaque-replacement (get-in response [:result :metaToUse]))))))
+      (is (= opaque-replacement (get-in response [:result :output :metaToUse]))))))
 
 (deftest test-hooks-pre-mcp-tool-call-output-meta-to-use-null
   (testing "preMcpToolCall: :meta-to-use nil serializes as JSON null (key present with null value)"
@@ -4483,11 +4604,11 @@
                                                     :cwd "/workspace"
                                                     :sessionId session-id}})]
       ;; The metaToUse key MUST be present (not absent) and its value MUST be null.
-      (is (contains? (:result response) :metaToUse))
-      (is (nil? (get-in response [:result :metaToUse]))))))
+      (is (contains? (get-in response [:result :output]) :metaToUse))
+      (is (nil? (get-in response [:result :output :metaToUse]))))))
 
 (deftest test-hooks-pre-mcp-tool-call-output-no-meta-to-use
-  (testing "preMcpToolCall: handler returning {} or nil omits metaToUse field"
+  (testing "preMcpToolCall: handler returning {} omits metaToUse field"
     (let [session (sdk/create-session *test-client*
                                       {:on-permission-request sdk/approve-all
                                        :hooks {:on-pre-mcp-tool-call
@@ -4503,7 +4624,8 @@
                                                     :timestamp 12345
                                                     :cwd "/workspace"
                                                     :sessionId session-id}})]
-      (is (not (contains? (:result response) :metaToUse))))))
+      (is (= {:output {}} (:result response)))
+      (is (not (contains? (get-in response [:result :output]) :metaToUse))))))
 
 ;; -----------------------------------------------------------------------------
 ;; User Input Handler Tests (server→client RPC)
@@ -6239,14 +6361,7 @@
       (is (= "application/octet-stream"
              (:mime-type (first (:binary-results-for-llm result))))))))
 
-;; --- AbortReason / SubagentStartedData.model (upstream PR #1225 codegen) -----
-
-(deftest test-abort-reason-enum
-  (testing "wire spec for abort reason is a closed enum"
-    (is (s/valid? :github.copilot-sdk.generated.event-specs/reason "user_initiated"))
-    (is (s/valid? :github.copilot-sdk.generated.event-specs/reason "remote_command"))
-    (is (s/valid? :github.copilot-sdk.generated.event-specs/reason "user_abort"))
-    (is (not (s/valid? :github.copilot-sdk.generated.event-specs/reason "arbitrary_reason")))))
+;; --- SubagentStartedData.model (upstream PR #1225 codegen) -------------------
 
 (deftest test-subagent-started-model-field
   (testing "idiom ::subagent.started-data spec accepts optional :model"