Repository navigation
feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits - #1825
Conversation
… durability, access, plugins, transports, integrations, and coding tools
…ities read from that connection, and no private SDK field
…bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId
…ew methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types
# Conflicts: # packages/ai-harness/tests/view-paths.test.ts
…ges, the fixed durable-tools and server-tools text, and a known limits section
|
| Name | Type |
|---|---|
| @tanstack/ai-harness | Minor |
| @tanstack/ai-mcp | Minor |
| @tanstack/ai-sandbox | Minor |
| @tanstack/ai-code-mode | Minor |
| @tanstack/ai-acp | Minor |
| @tanstack/ai | Minor |
| @tanstack/ai-harness-cli | Patch |
| @tanstack/ai-skills | Patch |
| @tanstack/ai-claude-code | Patch |
| @tanstack/ai-codex | Patch |
| @tanstack/ai-durable-stream | Patch |
| @tanstack/ai-grok-build | Patch |
| @tanstack/ai-opencode | Patch |
| @tanstack/ai-sandbox-blaxel | Patch |
| @tanstack/ai-sandbox-boxd | Patch |
| @tanstack/ai-sandbox-cloudflare | Patch |
| @tanstack/ai-sandbox-daytona | Patch |
| @tanstack/ai-sandbox-docker | Patch |
| @tanstack/ai-sandbox-e2b | Patch |
| @tanstack/ai-sandbox-local-process | Patch |
| @tanstack/ai-sandbox-railway | Patch |
| @tanstack/ai-sandbox-sprites | Patch |
| @tanstack/ai-sandbox-upstash-box | Patch |
| @tanstack/ai-sandbox-vercel | Patch |
| @tanstack/ai-code-mode-snippets | Patch |
| @tanstack/ai-isolate-cloudflare | Patch |
| @tanstack/ai-isolate-daytona | Patch |
| @tanstack/ai-isolate-e2b | Patch |
| @tanstack/ai-isolate-node | Patch |
| @tanstack/ai-isolate-quickjs-bun | Patch |
| @tanstack/ai-isolate-quickjs | Patch |
| @tanstack/ai-angular | Patch |
| @tanstack/ai-anthropic | Patch |
| @tanstack/ai-bedrock | Patch |
| @tanstack/ai-byteplus | Patch |
| @tanstack/ai-client | Patch |
| @tanstack/ai-cloudflare | Patch |
| @tanstack/ai-cohere | Patch |
| @tanstack/ai-compaction | Patch |
| @tanstack/ai-devtools-core | Patch |
| @tanstack/ai-elevenlabs | Patch |
| @tanstack/ai-fal | Patch |
| @tanstack/ai-gemini | Patch |
| @tanstack/ai-grok | Patch |
| @tanstack/ai-groq | Patch |
| @tanstack/ai-llmgateway | Patch |
| @tanstack/ai-lovable | Patch |
| @tanstack/ai-memory | Patch |
| @tanstack/ai-mistral | Patch |
| @tanstack/ai-octane | Patch |
| @tanstack/ai-ollama | Patch |
| @tanstack/ai-ollaya | Patch |
| @tanstack/ai-openai | Patch |
| @tanstack/ai-openrouter | Patch |
| @tanstack/ai-perplexity | Patch |
| @tanstack/ai-persistence | Patch |
| @tanstack/ai-preact | Patch |
| @tanstack/ai-react | Patch |
| @tanstack/ai-reactor | Patch |
| @tanstack/ai-remix | Patch |
| @tanstack/ai-solid | Patch |
| @tanstack/ai-svelte | Patch |
| @tanstack/ai-typesafe | Patch |
| @tanstack/ai-vercel-gateway | Patch |
| @tanstack/ai-vertex | Patch |
| @tanstack/ai-vue | Patch |
| @tanstack/ai-worldlabs | Patch |
| @tanstack/openai-base | Patch |
| ag-ui | Patch |
Click here to learn what changesets are, and how to add one.
Click here if you're a maintainer who wants to add a changeset to this PR
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit f15ba72
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-byteplus
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-cloudflare
@tanstack/ai-code-mode
@tanstack/ai-code-mode-snippets
@tanstack/ai-codex
@tanstack/ai-cohere
@tanstack/ai-compaction
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-harness
@tanstack/ai-harness-cli
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-daytona
@tanstack/ai-isolate-e2b
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-isolate-quickjs-bun
@tanstack/ai-llmgateway
@tanstack/ai-lovable
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-octane
@tanstack/ai-ollama
@tanstack/ai-ollaya
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-perplexity
@tanstack/ai-persistence
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-reactor
@tanstack/ai-remix
@tanstack/ai-sandbox
@tanstack/ai-sandbox-blaxel
@tanstack/ai-sandbox-boxd
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-e2b
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-railway
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-upstash-box
@tanstack/ai-sandbox-vercel
@tanstack/ai-skills
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-typesafe
@tanstack/ai-utils
@tanstack/ai-vercel-gateway
@tanstack/ai-vertex
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/ai-worldlabs
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
@tanstack/svelte-ai-devtools
commit: |
Coverage✅ Coverage held across 30 compared package(s). Each package is measured twice in this job — on this PR and on its merge-base with
|
…hread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull.
… revoke that rejects, and one known limit less
… plugin, and session.revert reads them the same way
…oval, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token
…lient, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui
…ess): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host
…eams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt.
…for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting.
… agent in the log, and host.transfer between harnesses (#1819) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * test(e2e): the handoff route reads view text by message role * feat(ai-harness, ai-sandbox, ai-sandbox-local-process): coding tools for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting. * feat(ai-harness, ai-persistence): an operator role, so the user who waits for a person cannot act as that person membersOnly() gives the operator action only to the role 'operator' by default. membersOnly({ operator: { roles } }) sets the roles that have it. A human wait takes only 'operator', for an answer and for a resolve that ends it, on a handle and through host.submit. * fix(ai-harness): handoff review fixes - The fold ends a handoff to a person when its human wait settles in any way, and refuses a human handoff record with no waitId. - The router writes the move to the main model once when the active agent is gone. - An operator can hand back to a root agent of a turn plugin: the human handoff record keeps the agents of its turn. - routing.handoffs: false gives no model the handoff tool. - A handoff stops only the chat that made it. - A handoff in a part that also stops for interrupts waits with them, so the resolve turn runs the target or pauses for a person. - The view adds no empty message for a handoff to a person or an operator hand-back. - An operator reply keeps its text in the assistant message and its files in a marked user note, which the view shows with the reply. - host.transfer throws for a source thread that does not exist. * fix(ai): ask onShouldContinue after the resumed tools, before the next model call A resume runs the pending tool calls, then went to the model with no loop check. Now the middleware onShouldContinue decides, as after any tool phase. false ends the run with no more model calls. So a harness handoff in a part that also waited for a client tool runs the target in the resolve turn, and the agent that handed off does not answer again.
…d budgets that hold, run inheritance, shared state, durable groups, and harnessAgent as a child run (#1818) * feat(ai-harness): typed subject-first agent actions start, run, and group, with grouped options, for plugins and agent code * feat(ai-harness): the run tree in the log, with RunNode for turns, agents, tools, and commands, and agentRuns read from it * feat(ai-harness): agent limits and budgets that hold, with queued starts, per-run usage, timeouts, attempts, and a budget code * feat(ai-harness): background runs inherit turn plugins, child signals follow their parent, tool-mode children get ctx.harness and share the turn budget, and ctx.harness.state shares state by key * feat(ai-harness): durable agent groups with a concurrency gate and keyed children, and routed agents that take steers, late joins, and turn hooks * docs(ai-harness): agents from code, groups, limits, budgets, the run tree, shared state, and routed agents * feat(ai-harness): harnessAgent runs as a child session in the root log, for the parent's user and keys, with usage, budget, waits, and events in the parent run * docs(ai-harness): the harnessAgent child run, as built * test(e2e): agent groups with a concurrency gate, a run budget, shared state, and a harnessAgent child run * test(ai-harness): give polled checks and a takeover deadline room for slower CI coverage runs * feat(ai-harness): handoffs between agents and to a person, the active agent in the log, and host.transfer between harnesses (#1819) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * test(e2e): the handoff route reads view text by message role * feat(ai-harness, ai-sandbox, ai-sandbox-local-process): coding tools for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting. * feat(ai-harness, ai-persistence): an operator role, so the user who waits for a person cannot act as that person membersOnly() gives the operator action only to the role 'operator' by default. membersOnly({ operator: { roles } }) sets the roles that have it. A human wait takes only 'operator', for an answer and for a resolve that ends it, on a handle and through host.submit. * fix(ai-harness): handoff review fixes - The fold ends a handoff to a person when its human wait settles in any way, and refuses a human handoff record with no waitId. - The router writes the move to the main model once when the active agent is gone. - An operator can hand back to a root agent of a turn plugin: the human handoff record keeps the agents of its turn. - routing.handoffs: false gives no model the handoff tool. - A handoff stops only the chat that made it. - A handoff in a part that also stops for interrupts waits with them, so the resolve turn runs the target or pauses for a person. - The view adds no empty message for a handoff to a person or an operator hand-back. - An operator reply keeps its text in the assistant message and its files in a marked user note, which the view shows with the reply. - host.transfer throws for a source thread that does not exist. * fix(ai): ask onShouldContinue after the resumed tools, before the next model call A resume runs the pending tool calls, then went to the model with no loop check. Now the middleware onShouldContinue decides, as after any tool phase. false ends the run with no more model calls. So a harness handoff in a part that also waited for a client tool runs the target in the resolve turn, and the agent that handed off does not answer again. * fix(ai-harness): a harnessAgent child goes on after any resume, and a POST run resume of its wait streams the parent run The parent run now listens for the next turn of the child session, so an answer that comes early, or a child that resumes by itself (an expired wait), no longer leaves the parent run waiting for good. An answer to a child wait returns a receipt that names the parent run, so the POST run handler streams that run to its end. * fix(ai-harness): a plugin start with parent: ctx.context.run is a child of that agent run A top-level agent run that awaited a plugin start could wait for good under maxConcurrent: the start queued behind the run that awaited it. With parent: ctx.context.run, the start is a child of the running agent run, as ctx.harness.agents starts it, and takes no maxConcurrent slot. * fix(ai-harness): child ids of a resumable run in their own namespaces, so keyed children never collide and an outliving child keeps its steps Keyed and positional children now get separate ids (start:k:<key> vs start:n:<n>, group:k:<key> vs group:n:<n>), after a '/' that the settle cleanup prefix agent:<parentInputId>: cannot match. So a background child that outlives its parent keeps its steps, tool results, and kept answers. A known child of another agent than the one asked for now throws. * fix(ai-harness): a resumed agent run carries the usage of the run before, so its budget holds across a restart The log fold now keeps the usage sum of a run whose input is still applied, and moves it to the next run of that input when it applies. So maxTokens and maxCost count every attempt of a resume: true run. * fix(ai-harness): onModelError of an agent run retries only the failed model call The option was asked for any failure of the run, and 'retry' ran the whole agent again from its first message, with no bound: a tool side effect ran twice, and a permanent error looped for good. Now a chat middleware asks the option only for a model call that fails before it streams, waits a short backoff, and calls that model again. Core calls one model call at most 3 times. * fix(ai-harness): host.open reserves only real harnessAgent child and harnessText inner thread ids host.open refused any thread id that contained /agent/ or /inner/. One helper now refuses such an id only when the part before the marker is a thread of the index, so an app id like org/agent/notes opens. * docs(ai-harness): document only the stable fields of harness.run.node, and mark its bookkeeping fields internal * chore(ai-harness): import the backoff wait under its own name, so it shadows no wait * refactor(ai-harness): one admission gate for maxConcurrent slots, group slots, and tree budget calls The session maxConcurrent slots, the slots of each group, and the tree budget calls that ran when a queued child started were three separate mechanisms. A small Gate now holds the slots of both the session and a group, with FIFO waiters, drop on abort, and an admit hook that takes the tree budget call as a start enters. admitStart replaces reserveWhenReady. No behavior change. * feat(ai-harness): a plugin start in a tool call of an agent run is a child of that run, with parent: null to opt out ctx.agents of a plugin now finds the agent run whose tool call runs (when it is the only operation of the session in a tool call), and starts the agent as its child, so it takes no maxConcurrent slot and the run can await it. parent: { runId } still names the run, and parent: null starts a top-level run that takes a slot. * fix(ai-harness): refuse every /inner/ thread id from app code host.open, host.submit, host.fork, and host.transfer now refuse any thread id with /inner/, also when the thread before it does not exist yet, so no app call can take the id of a harnessText inner session first. Only the throughOuterThread open gets one. The narrow /agent/ rule stays. * fix(ai-harness): a stop of the parent run cancels its harnessAgent child again The change that lets a child go on after any resume dropped the abort listener of the parent run. A stopped run then left its child running, and a run stopped while its child waited never ended. * test(ai-harness): skip four flaky CI tests, and no backoff after the last onModelError retry
…n the log, at the boundary, in background runs, and on every transport (#1817) * feat(ai-harness): the Wait type and waits in the log, with expiry, waitFor, settled({ inputId, signal }), and operator answers to human waits * wip(ai-harness, ai-mcp): PR 7 boundary, coding-approvals, live-waits, background-waits (stopped mid-way, not checked) * feat(ai-harness, ai-mcp): waits at the boundary (permissions, the question tool, step.ask), live MCP and sign-in waits, and background runs that suspend and resume * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): agent runs pause on boundary approvals through SubagentBinding.interrupts, and every transport uses one wait shape * test(ai-mcp): the media result has a waits list * test(e2e): an approval asked on one server is answered on another, and an unanswered wait expires * docs(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): wait for a person, step.ask, approvals at the boundary, background waits, and waits on every transport * fix(ai-harness): a session that does not own the log shows the waits and interrupts other hosts write * fix(ai-harness): lint errors in agent waits, step.ask, and permission resources * test(ai-harness, ai-code-mode): approvals are turn waits answered with resolve, and the device code carries expiresAt * test(e2e): in acceptEdits mode the write asks before it runs, with its formatter, and a reject writes nothing * feat(ai-harness): the agent tree, with typed agent actions, limits and budgets that hold, run inheritance, shared state, durable groups, and harnessAgent as a child run (#1818) * feat(ai-harness): typed subject-first agent actions start, run, and group, with grouped options, for plugins and agent code * feat(ai-harness): the run tree in the log, with RunNode for turns, agents, tools, and commands, and agentRuns read from it * feat(ai-harness): agent limits and budgets that hold, with queued starts, per-run usage, timeouts, attempts, and a budget code * feat(ai-harness): background runs inherit turn plugins, child signals follow their parent, tool-mode children get ctx.harness and share the turn budget, and ctx.harness.state shares state by key * feat(ai-harness): durable agent groups with a concurrency gate and keyed children, and routed agents that take steers, late joins, and turn hooks * docs(ai-harness): agents from code, groups, limits, budgets, the run tree, shared state, and routed agents * feat(ai-harness): harnessAgent runs as a child session in the root log, for the parent's user and keys, with usage, budget, waits, and events in the parent run * docs(ai-harness): the harnessAgent child run, as built * test(e2e): agent groups with a concurrency gate, a run budget, shared state, and a harnessAgent child run * test(ai-harness): give polled checks and a takeover deadline room for slower CI coverage runs * feat(ai-harness): handoffs between agents and to a person, the active agent in the log, and host.transfer between harnesses (#1819) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * test(e2e): the handoff route reads view text by message role * feat(ai-harness, ai-sandbox, ai-sandbox-local-process): coding tools for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting. * feat(ai-harness, ai-persistence): an operator role, so the user who waits for a person cannot act as that person membersOnly() gives the operator action only to the role 'operator' by default. membersOnly({ operator: { roles } }) sets the roles that have it. A human wait takes only 'operator', for an answer and for a resolve that ends it, on a handle and through host.submit. * fix(ai-harness): handoff review fixes - The fold ends a handoff to a person when its human wait settles in any way, and refuses a human handoff record with no waitId. - The router writes the move to the main model once when the active agent is gone. - An operator can hand back to a root agent of a turn plugin: the human handoff record keeps the agents of its turn. - routing.handoffs: false gives no model the handoff tool. - A handoff stops only the chat that made it. - A handoff in a part that also stops for interrupts waits with them, so the resolve turn runs the target or pauses for a person. - The view adds no empty message for a handoff to a person or an operator hand-back. - An operator reply keeps its text in the assistant message and its files in a marked user note, which the view shows with the reply. - host.transfer throws for a source thread that does not exist. * fix(ai): ask onShouldContinue after the resumed tools, before the next model call A resume runs the pending tool calls, then went to the model with no loop check. Now the middleware onShouldContinue decides, as after any tool phase. false ends the run with no more model calls. So a harness handoff in a part that also waited for a client tool runs the target in the resolve turn, and the agent that handed off does not answer again. * fix(ai-harness): a harnessAgent child goes on after any resume, and a POST run resume of its wait streams the parent run The parent run now listens for the next turn of the child session, so an answer that comes early, or a child that resumes by itself (an expired wait), no longer leaves the parent run waiting for good. An answer to a child wait returns a receipt that names the parent run, so the POST run handler streams that run to its end. * fix(ai-harness): a plugin start with parent: ctx.context.run is a child of that agent run A top-level agent run that awaited a plugin start could wait for good under maxConcurrent: the start queued behind the run that awaited it. With parent: ctx.context.run, the start is a child of the running agent run, as ctx.harness.agents starts it, and takes no maxConcurrent slot. * fix(ai-harness): child ids of a resumable run in their own namespaces, so keyed children never collide and an outliving child keeps its steps Keyed and positional children now get separate ids (start:k:<key> vs start:n:<n>, group:k:<key> vs group:n:<n>), after a '/' that the settle cleanup prefix agent:<parentInputId>: cannot match. So a background child that outlives its parent keeps its steps, tool results, and kept answers. A known child of another agent than the one asked for now throws. * fix(ai-harness): a resumed agent run carries the usage of the run before, so its budget holds across a restart The log fold now keeps the usage sum of a run whose input is still applied, and moves it to the next run of that input when it applies. So maxTokens and maxCost count every attempt of a resume: true run. * fix(ai-harness): onModelError of an agent run retries only the failed model call The option was asked for any failure of the run, and 'retry' ran the whole agent again from its first message, with no bound: a tool side effect ran twice, and a permanent error looped for good. Now a chat middleware asks the option only for a model call that fails before it streams, waits a short backoff, and calls that model again. Core calls one model call at most 3 times. * fix(ai-harness): host.open reserves only real harnessAgent child and harnessText inner thread ids host.open refused any thread id that contained /agent/ or /inner/. One helper now refuses such an id only when the part before the marker is a thread of the index, so an app id like org/agent/notes opens. * docs(ai-harness): document only the stable fields of harness.run.node, and mark its bookkeeping fields internal * chore(ai-harness): import the backoff wait under its own name, so it shadows no wait * refactor(ai-harness): one admission gate for maxConcurrent slots, group slots, and tree budget calls The session maxConcurrent slots, the slots of each group, and the tree budget calls that ran when a queued child started were three separate mechanisms. A small Gate now holds the slots of both the session and a group, with FIFO waiters, drop on abort, and an admit hook that takes the tree budget call as a start enters. admitStart replaces reserveWhenReady. No behavior change. * feat(ai-harness): a plugin start in a tool call of an agent run is a child of that run, with parent: null to opt out ctx.agents of a plugin now finds the agent run whose tool call runs (when it is the only operation of the session in a tool call), and starts the agent as its child, so it takes no maxConcurrent slot and the run can await it. parent: { runId } still names the run, and parent: null starts a top-level run that takes a slot. * fix(ai-harness): refuse every /inner/ thread id from app code host.open, host.submit, host.fork, and host.transfer now refuse any thread id with /inner/, also when the thread before it does not exist yet, so no app call can take the id of a harnessText inner session first. Only the throughOuterThread open gets one. The narrow /agent/ rule stays. * fix(ai-harness): a stop of the parent run cancels its harnessAgent child again The change that lets a child go on after any resume dropped the abort listener of the parent run. A stopped run then left its child running, and a run stopped while its child waited never ended. * test(ai-harness): skip four flaky CI tests, and no backoff after the last onModelError retry * feat(ai): export validateJsonSchema and the Standard Schema types * fix(ai-harness): recovery fails an interrupted agent run whose waits never reached the log, so it does not wait forever * fix(ai-harness, ai-mcp, ai-acp, ai-harness-cli): one wait view, and answer and resolve check every wait they end - snapshot().pendingWaits is the one wait view. pendingInterrupts and pendingQuestions are gone. A PendingWait has its interrupt when a chat run stopped for it, turn: true for a wait of the stopped turn, and the secret and url of a question. The HTTP connect payload, the view, MCP, ACP, the CLI, and the e2e apps read it. - The answer to a logged wait is checked with validateJsonSchema of core (cfworker), so pattern and every other keyword count. The hand-written JSON Schema validator and the copied Standard Schema types are gone. - resolve takes only waits with a pending interrupt (use_answer for a step.ask question or a handoff wait), and only the waits of one run (mixed_runs). host.submit checks a resolve against the run of its wait. - A second answer to a wait that an answer ends now is refused. - The MCP resolve tool finds a background-run wait in the log and checks every decision, so it answers a wait that another server opened. * test(e2e): read only this test's request from the aimock journal in the ArkType wire spec Other specs run at the same time against the same aimock. The spec read entries[0], which was a request of a parallel spec, so it got no tool. It now finds its request by the x-test-id header, and it no longer clears the journal that the other specs read. * fix(ai-harness): no dropped-handle warning after host.close() host.close() closes every session, but it kept the hold records. When the garbage collector later dropped a handle that was not closed, the host warned. The warning came at a random time, also while vitest closed a test file, which fails the run with 'Closing rpc while onUserConsoleLog was pending'. The host now gives no warning after close(). A test in client-reads also closes the handle that it opens.
…lt in the log, secret answers by reference, and single-flight OAuth refresh (#1815) * feat(ai): CredentialStore compare-and-set pair, readCredentialEntry, and one scrubSecrets * feat(ai-harness): one redactor per session for log records, feed events, plugin state, notes, and the model context * feat(ai-harness): the log keeps the final tool result the model saw, and errors and step results leave the session scrubbed * feat(ai-harness): a secret answer sent to another host goes through the credential store as a one-time reference * test(e2e): a secret answer sent to another host never shows in the transcript, the events, the log, or the model input * feat(ai-harness, ai-mcp): OAuth refresh runs once per host, saves with compare-and-set to the scope it came from, and keeps the client registration * docs(ai, ai-harness, ai-mcp): redaction, secret answers across hosts, the final tool result, OAuth refresh, and the CredentialStore compare-and-set pair * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): durable waits in the log, at the boundary, in background runs, and on every transport (#1817) * feat(ai-harness): the Wait type and waits in the log, with expiry, waitFor, settled({ inputId, signal }), and operator answers to human waits * wip(ai-harness, ai-mcp): PR 7 boundary, coding-approvals, live-waits, background-waits (stopped mid-way, not checked) * feat(ai-harness, ai-mcp): waits at the boundary (permissions, the question tool, step.ask), live MCP and sign-in waits, and background runs that suspend and resume * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): agent runs pause on boundary approvals through SubagentBinding.interrupts, and every transport uses one wait shape * test(ai-mcp): the media result has a waits list * test(e2e): an approval asked on one server is answered on another, and an unanswered wait expires * docs(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): wait for a person, step.ask, approvals at the boundary, background waits, and waits on every transport * fix(ai-harness): a session that does not own the log shows the waits and interrupts other hosts write * fix(ai-harness): lint errors in agent waits, step.ask, and permission resources * test(ai-harness, ai-code-mode): approvals are turn waits answered with resolve, and the device code carries expiresAt * test(e2e): in acceptEdits mode the write asks before it runs, with its formatter, and a reject writes nothing * feat(ai-harness): the agent tree, with typed agent actions, limits and budgets that hold, run inheritance, shared state, durable groups, and harnessAgent as a child run (#1818) * feat(ai-harness): typed subject-first agent actions start, run, and group, with grouped options, for plugins and agent code * feat(ai-harness): the run tree in the log, with RunNode for turns, agents, tools, and commands, and agentRuns read from it * feat(ai-harness): agent limits and budgets that hold, with queued starts, per-run usage, timeouts, attempts, and a budget code * feat(ai-harness): background runs inherit turn plugins, child signals follow their parent, tool-mode children get ctx.harness and share the turn budget, and ctx.harness.state shares state by key * feat(ai-harness): durable agent groups with a concurrency gate and keyed children, and routed agents that take steers, late joins, and turn hooks * docs(ai-harness): agents from code, groups, limits, budgets, the run tree, shared state, and routed agents * feat(ai-harness): harnessAgent runs as a child session in the root log, for the parent's user and keys, with usage, budget, waits, and events in the parent run * docs(ai-harness): the harnessAgent child run, as built * test(e2e): agent groups with a concurrency gate, a run budget, shared state, and a harnessAgent child run * test(ai-harness): give polled checks and a takeover deadline room for slower CI coverage runs * feat(ai-harness): handoffs between agents and to a person, the active agent in the log, and host.transfer between harnesses (#1819) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * test(e2e): the handoff route reads view text by message role * feat(ai-harness, ai-sandbox, ai-sandbox-local-process): coding tools for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting. * feat(ai-harness, ai-persistence): an operator role, so the user who waits for a person cannot act as that person membersOnly() gives the operator action only to the role 'operator' by default. membersOnly({ operator: { roles } }) sets the roles that have it. A human wait takes only 'operator', for an answer and for a resolve that ends it, on a handle and through host.submit. * fix(ai-harness): handoff review fixes - The fold ends a handoff to a person when its human wait settles in any way, and refuses a human handoff record with no waitId. - The router writes the move to the main model once when the active agent is gone. - An operator can hand back to a root agent of a turn plugin: the human handoff record keeps the agents of its turn. - routing.handoffs: false gives no model the handoff tool. - A handoff stops only the chat that made it. - A handoff in a part that also stops for interrupts waits with them, so the resolve turn runs the target or pauses for a person. - The view adds no empty message for a handoff to a person or an operator hand-back. - An operator reply keeps its text in the assistant message and its files in a marked user note, which the view shows with the reply. - host.transfer throws for a source thread that does not exist. * fix(ai): ask onShouldContinue after the resumed tools, before the next model call A resume runs the pending tool calls, then went to the model with no loop check. Now the middleware onShouldContinue decides, as after any tool phase. false ends the run with no more model calls. So a harness handoff in a part that also waited for a client tool runs the target in the resolve turn, and the agent that handed off does not answer again. * fix(ai-harness): a harnessAgent child goes on after any resume, and a POST run resume of its wait streams the parent run The parent run now listens for the next turn of the child session, so an answer that comes early, or a child that resumes by itself (an expired wait), no longer leaves the parent run waiting for good. An answer to a child wait returns a receipt that names the parent run, so the POST run handler streams that run to its end. * fix(ai-harness): a plugin start with parent: ctx.context.run is a child of that agent run A top-level agent run that awaited a plugin start could wait for good under maxConcurrent: the start queued behind the run that awaited it. With parent: ctx.context.run, the start is a child of the running agent run, as ctx.harness.agents starts it, and takes no maxConcurrent slot. * fix(ai-harness): child ids of a resumable run in their own namespaces, so keyed children never collide and an outliving child keeps its steps Keyed and positional children now get separate ids (start:k:<key> vs start:n:<n>, group:k:<key> vs group:n:<n>), after a '/' that the settle cleanup prefix agent:<parentInputId>: cannot match. So a background child that outlives its parent keeps its steps, tool results, and kept answers. A known child of another agent than the one asked for now throws. * fix(ai-harness): a resumed agent run carries the usage of the run before, so its budget holds across a restart The log fold now keeps the usage sum of a run whose input is still applied, and moves it to the next run of that input when it applies. So maxTokens and maxCost count every attempt of a resume: true run. * fix(ai-harness): onModelError of an agent run retries only the failed model call The option was asked for any failure of the run, and 'retry' ran the whole agent again from its first message, with no bound: a tool side effect ran twice, and a permanent error looped for good. Now a chat middleware asks the option only for a model call that fails before it streams, waits a short backoff, and calls that model again. Core calls one model call at most 3 times. * fix(ai-harness): host.open reserves only real harnessAgent child and harnessText inner thread ids host.open refused any thread id that contained /agent/ or /inner/. One helper now refuses such an id only when the part before the marker is a thread of the index, so an app id like org/agent/notes opens. * docs(ai-harness): document only the stable fields of harness.run.node, and mark its bookkeeping fields internal * chore(ai-harness): import the backoff wait under its own name, so it shadows no wait * refactor(ai-harness): one admission gate for maxConcurrent slots, group slots, and tree budget calls The session maxConcurrent slots, the slots of each group, and the tree budget calls that ran when a queued child started were three separate mechanisms. A small Gate now holds the slots of both the session and a group, with FIFO waiters, drop on abort, and an admit hook that takes the tree budget call as a start enters. admitStart replaces reserveWhenReady. No behavior change. * feat(ai-harness): a plugin start in a tool call of an agent run is a child of that run, with parent: null to opt out ctx.agents of a plugin now finds the agent run whose tool call runs (when it is the only operation of the session in a tool call), and starts the agent as its child, so it takes no maxConcurrent slot and the run can await it. parent: { runId } still names the run, and parent: null starts a top-level run that takes a slot. * fix(ai-harness): refuse every /inner/ thread id from app code host.open, host.submit, host.fork, and host.transfer now refuse any thread id with /inner/, also when the thread before it does not exist yet, so no app call can take the id of a harnessText inner session first. Only the throughOuterThread open gets one. The narrow /agent/ rule stays. * fix(ai-harness): a stop of the parent run cancels its harnessAgent child again The change that lets a child go on after any resume dropped the abort listener of the parent run. A stopped run then left its child running, and a run stopped while its child waited never ended. * test(ai-harness): skip four flaky CI tests, and no backoff after the last onModelError retry * feat(ai): export validateJsonSchema and the Standard Schema types * fix(ai-harness): recovery fails an interrupted agent run whose waits never reached the log, so it does not wait forever * fix(ai-harness, ai-mcp, ai-acp, ai-harness-cli): one wait view, and answer and resolve check every wait they end - snapshot().pendingWaits is the one wait view. pendingInterrupts and pendingQuestions are gone. A PendingWait has its interrupt when a chat run stopped for it, turn: true for a wait of the stopped turn, and the secret and url of a question. The HTTP connect payload, the view, MCP, ACP, the CLI, and the e2e apps read it. - The answer to a logged wait is checked with validateJsonSchema of core (cfworker), so pattern and every other keyword count. The hand-written JSON Schema validator and the copied Standard Schema types are gone. - resolve takes only waits with a pending interrupt (use_answer for a step.ask question or a handoff wait), and only the waits of one run (mixed_runs). host.submit checks a resolve against the run of its wait. - A second answer to a wait that an answer ends now is refused. - The MCP resolve tool finds a background-run wait in the log and checks every decision, so it answers a wait that another server opened. * test(e2e): read only this test's request from the aimock journal in the ArkType wire spec Other specs run at the same time against the same aimock. The spec read entries[0], which was a request of a parallel spec, so it got no tool. It now finds its request by the x-test-id header, and it no longer clears the journal that the other specs read. * fix(ai-harness): no dropped-handle warning after host.close() host.close() closes every session, but it kept the hold records. When the garbage collector later dropped a handle that was not closed, the host warned. The warning came at a random time, also while vitest closed a test file, which fails the run with 'Closing rpc while onUserConsoleLog was pending'. The host now gives no warning after close(). A test in client-reads also closes the handle that it opens. * feat(ai): add lastChatMiddleware to SubagentBinding, and the secret_answer credential type - binding.lastChatMiddleware runs after the own middleware of each ctx.chat call of an agent run, so a host middleware sees the final model context and tool results. Nested children get it after their own. - Credential has a secret_answer type with an expiresAt, so the harness keeps a secret answer apart from the user credentials. * fix(ai-harness): keep secrets out of step values, agent middleware, and secret answer rows - step.do throws when its value holds a secret of the session, on the first run and on a replay, so the log never keeps a secret and a tool never gets a silent '[redacted]' value. - The redact, tool result scrub, and tool result recorder middleware of an agent run go in binding.lastChatMiddleware, after the agent's own ctx.chat middleware. The log keeps the result the model got, and an agent middleware that sets the messages again cannot send a secret to the model. - Secret answers are credentials of the type secret_answer with an expiresAt 24 hours later. credentials.list hides that type. An expired ref is refused. - The redactor is a closure. Its walk covers Map and Set, and add() takes each string field of a secret answer with fields. - The duplicate check compares an input without the secrets of the session, as the log keeps it, so a retry with a secret in it is a duplicate. * fix(ai-mcp): ask for a sign-in when the user signs out while the SDK refreshes a token The SDK's new tokens are not saved when credentials.refresh finds no credential, so saveTokens throws AuthRequiredError instead of dropping them with no sign. * test(e2e): run the harness secret answer route against aimock The route uses the OpenAI adapter against aimock with a fixture, and the spec reads the model calls of each host from the aimock journal. * docs(ai-harness): narrow credentials on oauth, so the secret_answer kind type-checks
…h ownership, an input lane, workers, schedules, and snapshots (#1814) * feat(ai-harness): thread-state log records and their fold * feat(ai-persistence, ai-harness): one persistence shape, every host has a log * feat(ai-harness): settings, config, plugin state, interrupted turns, overrides, and agent results live in the log * feat(ai-harness): one ownership epoch per log, renewed once per host, with abort on loss * feat(ai-harness): runs and interrupts live in the session log * feat(ai-persistence, ai-harness): inputs from any machine through the log input lane, host.submit, and no inbox, work claims, or leases * feat(ai-harness): createHarnessWorker, session.schedule, and step.sleep on the log * test(e2e): a worker on another host runs a submitted input and a schedule * feat(ai-harness): one log per session tree, session.logState(), and a wake when a busy session closes * feat(ai-harness): snapshots in the log store by size or time, retention logRecords, and the harness.gap event * fix(ai-harness): export LogOwnedElsewhereError, and say that the log keeps ephemeral messages with the turn * docs(ai-harness, ai-persistence): durable sessions, one log per tree, workers, snapshots, and the changesets for the log as the session * ci: apply automated fixes * fix(ai-harness): check answers and cancels from submit and schedule against their run, a schedule against its maker, and keep a same-host takeover and a lost log's inputs working * test(ai-harness): give polled checks 5 s, for slower CI coverage runs * test(e2e): read the data lines of POST run streams that now carry an id line per event * feat(ai, ai-harness, ai-mcp): one redaction sink, the final tool result in the log, secret answers by reference, and single-flight OAuth refresh (#1815) * feat(ai): CredentialStore compare-and-set pair, readCredentialEntry, and one scrubSecrets * feat(ai-harness): one redactor per session for log records, feed events, plugin state, notes, and the model context * feat(ai-harness): the log keeps the final tool result the model saw, and errors and step results leave the session scrubbed * feat(ai-harness): a secret answer sent to another host goes through the credential store as a one-time reference * test(e2e): a secret answer sent to another host never shows in the transcript, the events, the log, or the model input * feat(ai-harness, ai-mcp): OAuth refresh runs once per host, saves with compare-and-set to the scope it came from, and keeps the client registration * docs(ai, ai-harness, ai-mcp): redaction, secret answers across hosts, the final tool result, OAuth refresh, and the CredentialStore compare-and-set pair * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): durable waits in the log, at the boundary, in background runs, and on every transport (#1817) * feat(ai-harness): the Wait type and waits in the log, with expiry, waitFor, settled({ inputId, signal }), and operator answers to human waits * wip(ai-harness, ai-mcp): PR 7 boundary, coding-approvals, live-waits, background-waits (stopped mid-way, not checked) * feat(ai-harness, ai-mcp): waits at the boundary (permissions, the question tool, step.ask), live MCP and sign-in waits, and background runs that suspend and resume * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): agent runs pause on boundary approvals through SubagentBinding.interrupts, and every transport uses one wait shape * test(ai-mcp): the media result has a waits list * test(e2e): an approval asked on one server is answered on another, and an unanswered wait expires * docs(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): wait for a person, step.ask, approvals at the boundary, background waits, and waits on every transport * fix(ai-harness): a session that does not own the log shows the waits and interrupts other hosts write * fix(ai-harness): lint errors in agent waits, step.ask, and permission resources * test(ai-harness, ai-code-mode): approvals are turn waits answered with resolve, and the device code carries expiresAt * test(e2e): in acceptEdits mode the write asks before it runs, with its formatter, and a reject writes nothing * feat(ai-harness): the agent tree, with typed agent actions, limits and budgets that hold, run inheritance, shared state, durable groups, and harnessAgent as a child run (#1818) * feat(ai-harness): typed subject-first agent actions start, run, and group, with grouped options, for plugins and agent code * feat(ai-harness): the run tree in the log, with RunNode for turns, agents, tools, and commands, and agentRuns read from it * feat(ai-harness): agent limits and budgets that hold, with queued starts, per-run usage, timeouts, attempts, and a budget code * feat(ai-harness): background runs inherit turn plugins, child signals follow their parent, tool-mode children get ctx.harness and share the turn budget, and ctx.harness.state shares state by key * feat(ai-harness): durable agent groups with a concurrency gate and keyed children, and routed agents that take steers, late joins, and turn hooks * docs(ai-harness): agents from code, groups, limits, budgets, the run tree, shared state, and routed agents * feat(ai-harness): harnessAgent runs as a child session in the root log, for the parent's user and keys, with usage, budget, waits, and events in the parent run * docs(ai-harness): the harnessAgent child run, as built * test(e2e): agent groups with a concurrency gate, a run budget, shared state, and a harnessAgent child run * test(ai-harness): give polled checks and a takeover deadline room for slower CI coverage runs * feat(ai-harness): handoffs between agents and to a person, the active agent in the log, and host.transfer between harnesses (#1819) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * test(e2e): the handoff route reads view text by message role * feat(ai-harness, ai-sandbox, ai-sandbox-local-process): coding tools for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting. * feat(ai-harness, ai-persistence): an operator role, so the user who waits for a person cannot act as that person membersOnly() gives the operator action only to the role 'operator' by default. membersOnly({ operator: { roles } }) sets the roles that have it. A human wait takes only 'operator', for an answer and for a resolve that ends it, on a handle and through host.submit. * fix(ai-harness): handoff review fixes - The fold ends a handoff to a person when its human wait settles in any way, and refuses a human handoff record with no waitId. - The router writes the move to the main model once when the active agent is gone. - An operator can hand back to a root agent of a turn plugin: the human handoff record keeps the agents of its turn. - routing.handoffs: false gives no model the handoff tool. - A handoff stops only the chat that made it. - A handoff in a part that also stops for interrupts waits with them, so the resolve turn runs the target or pauses for a person. - The view adds no empty message for a handoff to a person or an operator hand-back. - An operator reply keeps its text in the assistant message and its files in a marked user note, which the view shows with the reply. - host.transfer throws for a source thread that does not exist. * fix(ai): ask onShouldContinue after the resumed tools, before the next model call A resume runs the pending tool calls, then went to the model with no loop check. Now the middleware onShouldContinue decides, as after any tool phase. false ends the run with no more model calls. So a harness handoff in a part that also waited for a client tool runs the target in the resolve turn, and the agent that handed off does not answer again. * fix(ai-harness): a harnessAgent child goes on after any resume, and a POST run resume of its wait streams the parent run The parent run now listens for the next turn of the child session, so an answer that comes early, or a child that resumes by itself (an expired wait), no longer leaves the parent run waiting for good. An answer to a child wait returns a receipt that names the parent run, so the POST run handler streams that run to its end. * fix(ai-harness): a plugin start with parent: ctx.context.run is a child of that agent run A top-level agent run that awaited a plugin start could wait for good under maxConcurrent: the start queued behind the run that awaited it. With parent: ctx.context.run, the start is a child of the running agent run, as ctx.harness.agents starts it, and takes no maxConcurrent slot. * fix(ai-harness): child ids of a resumable run in their own namespaces, so keyed children never collide and an outliving child keeps its steps Keyed and positional children now get separate ids (start:k:<key> vs start:n:<n>, group:k:<key> vs group:n:<n>), after a '/' that the settle cleanup prefix agent:<parentInputId>: cannot match. So a background child that outlives its parent keeps its steps, tool results, and kept answers. A known child of another agent than the one asked for now throws. * fix(ai-harness): a resumed agent run carries the usage of the run before, so its budget holds across a restart The log fold now keeps the usage sum of a run whose input is still applied, and moves it to the next run of that input when it applies. So maxTokens and maxCost count every attempt of a resume: true run. * fix(ai-harness): onModelError of an agent run retries only the failed model call The option was asked for any failure of the run, and 'retry' ran the whole agent again from its first message, with no bound: a tool side effect ran twice, and a permanent error looped for good. Now a chat middleware asks the option only for a model call that fails before it streams, waits a short backoff, and calls that model again. Core calls one model call at most 3 times. * fix(ai-harness): host.open reserves only real harnessAgent child and harnessText inner thread ids host.open refused any thread id that contained /agent/ or /inner/. One helper now refuses such an id only when the part before the marker is a thread of the index, so an app id like org/agent/notes opens. * docs(ai-harness): document only the stable fields of harness.run.node, and mark its bookkeeping fields internal * chore(ai-harness): import the backoff wait under its own name, so it shadows no wait * refactor(ai-harness): one admission gate for maxConcurrent slots, group slots, and tree budget calls The session maxConcurrent slots, the slots of each group, and the tree budget calls that ran when a queued child started were three separate mechanisms. A small Gate now holds the slots of both the session and a group, with FIFO waiters, drop on abort, and an admit hook that takes the tree budget call as a start enters. admitStart replaces reserveWhenReady. No behavior change. * feat(ai-harness): a plugin start in a tool call of an agent run is a child of that run, with parent: null to opt out ctx.agents of a plugin now finds the agent run whose tool call runs (when it is the only operation of the session in a tool call), and starts the agent as its child, so it takes no maxConcurrent slot and the run can await it. parent: { runId } still names the run, and parent: null starts a top-level run that takes a slot. * fix(ai-harness): refuse every /inner/ thread id from app code host.open, host.submit, host.fork, and host.transfer now refuse any thread id with /inner/, also when the thread before it does not exist yet, so no app call can take the id of a harnessText inner session first. Only the throughOuterThread open gets one. The narrow /agent/ rule stays. * fix(ai-harness): a stop of the parent run cancels its harnessAgent child again The change that lets a child go on after any resume dropped the abort listener of the parent run. A stopped run then left its child running, and a run stopped while its child waited never ended. * test(ai-harness): skip four flaky CI tests, and no backoff after the last onModelError retry * feat(ai): export validateJsonSchema and the Standard Schema types * fix(ai-harness): recovery fails an interrupted agent run whose waits never reached the log, so it does not wait forever * fix(ai-harness, ai-mcp, ai-acp, ai-harness-cli): one wait view, and answer and resolve check every wait they end - snapshot().pendingWaits is the one wait view. pendingInterrupts and pendingQuestions are gone. A PendingWait has its interrupt when a chat run stopped for it, turn: true for a wait of the stopped turn, and the secret and url of a question. The HTTP connect payload, the view, MCP, ACP, the CLI, and the e2e apps read it. - The answer to a logged wait is checked with validateJsonSchema of core (cfworker), so pattern and every other keyword count. The hand-written JSON Schema validator and the copied Standard Schema types are gone. - resolve takes only waits with a pending interrupt (use_answer for a step.ask question or a handoff wait), and only the waits of one run (mixed_runs). host.submit checks a resolve against the run of its wait. - A second answer to a wait that an answer ends now is refused. - The MCP resolve tool finds a background-run wait in the log and checks every decision, so it answers a wait that another server opened. * test(e2e): read only this test's request from the aimock journal in the ArkType wire spec Other specs run at the same time against the same aimock. The spec read entries[0], which was a request of a parallel spec, so it got no tool. It now finds its request by the x-test-id header, and it no longer clears the journal that the other specs read. * fix(ai-harness): no dropped-handle warning after host.close() host.close() closes every session, but it kept the hold records. When the garbage collector later dropped a handle that was not closed, the host warned. The warning came at a random time, also while vitest closed a test file, which fails the run with 'Closing rpc while onUserConsoleLog was pending'. The host now gives no warning after close(). A test in client-reads also closes the handle that it opens. * feat(ai): add lastChatMiddleware to SubagentBinding, and the secret_answer credential type - binding.lastChatMiddleware runs after the own middleware of each ctx.chat call of an agent run, so a host middleware sees the final model context and tool results. Nested children get it after their own. - Credential has a secret_answer type with an expiresAt, so the harness keeps a secret answer apart from the user credentials. * fix(ai-harness): keep secrets out of step values, agent middleware, and secret answer rows - step.do throws when its value holds a secret of the session, on the first run and on a replay, so the log never keeps a secret and a tool never gets a silent '[redacted]' value. - The redact, tool result scrub, and tool result recorder middleware of an agent run go in binding.lastChatMiddleware, after the agent's own ctx.chat middleware. The log keeps the result the model got, and an agent middleware that sets the messages again cannot send a secret to the model. - Secret answers are credentials of the type secret_answer with an expiresAt 24 hours later. credentials.list hides that type. An expired ref is refused. - The redactor is a closure. Its walk covers Map and Set, and add() takes each string field of a secret answer with fields. - The duplicate check compares an input without the secrets of the session, as the log keeps it, so a retry with a secret in it is a duplicate. * fix(ai-mcp): ask for a sign-in when the user signs out while the SDK refreshes a token The SDK's new tokens are not saved when credentials.refresh finds no credential, so saveTokens throws AuthRequiredError instead of dropping them with no sign. * test(e2e): run the harness secret answer route against aimock The route uses the OpenAI adapter against aimock with a fixture, and the spec reads the model calls of each host from the aimock journal. * docs(ai-harness): narrow credentials on oauth, so the secret_answer kind type-checks * fix(ai-persistence): keep the harness of the owner or the first offer for a log * fix(ai-harness): check the harness in host.submit, and throw when a child cannot find its root * fix(ai-harness): prune settled state from the thread fold, and change its maps in place * fix(ai-harness): take only JSON values from step.do * fix(ai-harness): back off a failing log in the worker, free the slot of a long sleep, and need media on a shared host * test(ai-harness): skip a build-stub test that times out under load * chore(changeset): note the review fixes of the log session * fix(ai-harness): need media only on a host with ownership, and warn once on another host --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
… host plugins, and a purging delete (#1813) * fix(ai-harness, ai-mcp): OAuth flows and commands stop on their signal * fix(ai-harness-cli): SIGINT and SIGTERM cancel the turn, then stop, in every mode * fix(ai-harness): one retention policy bounds every per-session collection, the feed, and the log tail * feat(ai-harness, ai-mcp, ai-acp, ai-harness-cli): reference-counted session handles with idle close and maxOpen * docs(ai-harness): lifecycle and retention, handles, observe, signals, and OAuth cancel * feat(ai-harness, ai-mcp, ai-acp): host.observe, a read-only session view, and read routes on it * test(ai-e2e): delete purges media and messages, and reads work after idle close * feat(ai-harness, ai-mcp): host-lifetime plugins, MCP stdio servers once per host, and delete that purges a thread * fix(ai-harness): definePlugin infers its definition only from the argument * fix(ai-harness): key host plugins by the harness definition, not its name * test(ai-mcp): open two sessions of one harness definition to share a stdio server * test(ai-harness, ai-harness-cli): give polled checks 5 s, for slower CI runs * feat(ai-persistence, ai-harness): the session log is the session, with ownership, an input lane, workers, schedules, and snapshots (#1814) * feat(ai-harness): thread-state log records and their fold * feat(ai-persistence, ai-harness): one persistence shape, every host has a log * feat(ai-harness): settings, config, plugin state, interrupted turns, overrides, and agent results live in the log * feat(ai-harness): one ownership epoch per log, renewed once per host, with abort on loss * feat(ai-harness): runs and interrupts live in the session log * feat(ai-persistence, ai-harness): inputs from any machine through the log input lane, host.submit, and no inbox, work claims, or leases * feat(ai-harness): createHarnessWorker, session.schedule, and step.sleep on the log * test(e2e): a worker on another host runs a submitted input and a schedule * feat(ai-harness): one log per session tree, session.logState(), and a wake when a busy session closes * feat(ai-harness): snapshots in the log store by size or time, retention logRecords, and the harness.gap event * fix(ai-harness): export LogOwnedElsewhereError, and say that the log keeps ephemeral messages with the turn * docs(ai-harness, ai-persistence): durable sessions, one log per tree, workers, snapshots, and the changesets for the log as the session * ci: apply automated fixes * fix(ai-harness): check answers and cancels from submit and schedule against their run, a schedule against its maker, and keep a same-host takeover and a lost log's inputs working * test(ai-harness): give polled checks 5 s, for slower CI coverage runs * test(e2e): read the data lines of POST run streams that now carry an id line per event * feat(ai, ai-harness, ai-mcp): one redaction sink, the final tool result in the log, secret answers by reference, and single-flight OAuth refresh (#1815) * feat(ai): CredentialStore compare-and-set pair, readCredentialEntry, and one scrubSecrets * feat(ai-harness): one redactor per session for log records, feed events, plugin state, notes, and the model context * feat(ai-harness): the log keeps the final tool result the model saw, and errors and step results leave the session scrubbed * feat(ai-harness): a secret answer sent to another host goes through the credential store as a one-time reference * test(e2e): a secret answer sent to another host never shows in the transcript, the events, the log, or the model input * feat(ai-harness, ai-mcp): OAuth refresh runs once per host, saves with compare-and-set to the scope it came from, and keeps the client registration * docs(ai, ai-harness, ai-mcp): redaction, secret answers across hosts, the final tool result, OAuth refresh, and the CredentialStore compare-and-set pair * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): durable waits in the log, at the boundary, in background runs, and on every transport (#1817) * feat(ai-harness): the Wait type and waits in the log, with expiry, waitFor, settled({ inputId, signal }), and operator answers to human waits * wip(ai-harness, ai-mcp): PR 7 boundary, coding-approvals, live-waits, background-waits (stopped mid-way, not checked) * feat(ai-harness, ai-mcp): waits at the boundary (permissions, the question tool, step.ask), live MCP and sign-in waits, and background runs that suspend and resume * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): agent runs pause on boundary approvals through SubagentBinding.interrupts, and every transport uses one wait shape * test(ai-mcp): the media result has a waits list * test(e2e): an approval asked on one server is answered on another, and an unanswered wait expires * docs(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): wait for a person, step.ask, approvals at the boundary, background waits, and waits on every transport * fix(ai-harness): a session that does not own the log shows the waits and interrupts other hosts write * fix(ai-harness): lint errors in agent waits, step.ask, and permission resources * test(ai-harness, ai-code-mode): approvals are turn waits answered with resolve, and the device code carries expiresAt * test(e2e): in acceptEdits mode the write asks before it runs, with its formatter, and a reject writes nothing * feat(ai-harness): the agent tree, with typed agent actions, limits and budgets that hold, run inheritance, shared state, durable groups, and harnessAgent as a child run (#1818) * feat(ai-harness): typed subject-first agent actions start, run, and group, with grouped options, for plugins and agent code * feat(ai-harness): the run tree in the log, with RunNode for turns, agents, tools, and commands, and agentRuns read from it * feat(ai-harness): agent limits and budgets that hold, with queued starts, per-run usage, timeouts, attempts, and a budget code * feat(ai-harness): background runs inherit turn plugins, child signals follow their parent, tool-mode children get ctx.harness and share the turn budget, and ctx.harness.state shares state by key * feat(ai-harness): durable agent groups with a concurrency gate and keyed children, and routed agents that take steers, late joins, and turn hooks * docs(ai-harness): agents from code, groups, limits, budgets, the run tree, shared state, and routed agents * feat(ai-harness): harnessAgent runs as a child session in the root log, for the parent's user and keys, with usage, budget, waits, and events in the parent run * docs(ai-harness): the harnessAgent child run, as built * test(e2e): agent groups with a concurrency gate, a run budget, shared state, and a harnessAgent child run * test(ai-harness): give polled checks and a takeover deadline room for slower CI coverage runs * feat(ai-harness): handoffs between agents and to a person, the active agent in the log, and host.transfer between harnesses (#1819) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * test(e2e): the handoff route reads view text by message role * feat(ai-harness, ai-sandbox, ai-sandbox-local-process): coding tools for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting. * feat(ai-harness, ai-persistence): an operator role, so the user who waits for a person cannot act as that person membersOnly() gives the operator action only to the role 'operator' by default. membersOnly({ operator: { roles } }) sets the roles that have it. A human wait takes only 'operator', for an answer and for a resolve that ends it, on a handle and through host.submit. * fix(ai-harness): handoff review fixes - The fold ends a handoff to a person when its human wait settles in any way, and refuses a human handoff record with no waitId. - The router writes the move to the main model once when the active agent is gone. - An operator can hand back to a root agent of a turn plugin: the human handoff record keeps the agents of its turn. - routing.handoffs: false gives no model the handoff tool. - A handoff stops only the chat that made it. - A handoff in a part that also stops for interrupts waits with them, so the resolve turn runs the target or pauses for a person. - The view adds no empty message for a handoff to a person or an operator hand-back. - An operator reply keeps its text in the assistant message and its files in a marked user note, which the view shows with the reply. - host.transfer throws for a source thread that does not exist. * fix(ai): ask onShouldContinue after the resumed tools, before the next model call A resume runs the pending tool calls, then went to the model with no loop check. Now the middleware onShouldContinue decides, as after any tool phase. false ends the run with no more model calls. So a harness handoff in a part that also waited for a client tool runs the target in the resolve turn, and the agent that handed off does not answer again. * fix(ai-harness): a harnessAgent child goes on after any resume, and a POST run resume of its wait streams the parent run The parent run now listens for the next turn of the child session, so an answer that comes early, or a child that resumes by itself (an expired wait), no longer leaves the parent run waiting for good. An answer to a child wait returns a receipt that names the parent run, so the POST run handler streams that run to its end. * fix(ai-harness): a plugin start with parent: ctx.context.run is a child of that agent run A top-level agent run that awaited a plugin start could wait for good under maxConcurrent: the start queued behind the run that awaited it. With parent: ctx.context.run, the start is a child of the running agent run, as ctx.harness.agents starts it, and takes no maxConcurrent slot. * fix(ai-harness): child ids of a resumable run in their own namespaces, so keyed children never collide and an outliving child keeps its steps Keyed and positional children now get separate ids (start:k:<key> vs start:n:<n>, group:k:<key> vs group:n:<n>), after a '/' that the settle cleanup prefix agent:<parentInputId>: cannot match. So a background child that outlives its parent keeps its steps, tool results, and kept answers. A known child of another agent than the one asked for now throws. * fix(ai-harness): a resumed agent run carries the usage of the run before, so its budget holds across a restart The log fold now keeps the usage sum of a run whose input is still applied, and moves it to the next run of that input when it applies. So maxTokens and maxCost count every attempt of a resume: true run. * fix(ai-harness): onModelError of an agent run retries only the failed model call The option was asked for any failure of the run, and 'retry' ran the whole agent again from its first message, with no bound: a tool side effect ran twice, and a permanent error looped for good. Now a chat middleware asks the option only for a model call that fails before it streams, waits a short backoff, and calls that model again. Core calls one model call at most 3 times. * fix(ai-harness): host.open reserves only real harnessAgent child and harnessText inner thread ids host.open refused any thread id that contained /agent/ or /inner/. One helper now refuses such an id only when the part before the marker is a thread of the index, so an app id like org/agent/notes opens. * docs(ai-harness): document only the stable fields of harness.run.node, and mark its bookkeeping fields internal * chore(ai-harness): import the backoff wait under its own name, so it shadows no wait * refactor(ai-harness): one admission gate for maxConcurrent slots, group slots, and tree budget calls The session maxConcurrent slots, the slots of each group, and the tree budget calls that ran when a queued child started were three separate mechanisms. A small Gate now holds the slots of both the session and a group, with FIFO waiters, drop on abort, and an admit hook that takes the tree budget call as a start enters. admitStart replaces reserveWhenReady. No behavior change. * feat(ai-harness): a plugin start in a tool call of an agent run is a child of that run, with parent: null to opt out ctx.agents of a plugin now finds the agent run whose tool call runs (when it is the only operation of the session in a tool call), and starts the agent as its child, so it takes no maxConcurrent slot and the run can await it. parent: { runId } still names the run, and parent: null starts a top-level run that takes a slot. * fix(ai-harness): refuse every /inner/ thread id from app code host.open, host.submit, host.fork, and host.transfer now refuse any thread id with /inner/, also when the thread before it does not exist yet, so no app call can take the id of a harnessText inner session first. Only the throughOuterThread open gets one. The narrow /agent/ rule stays. * fix(ai-harness): a stop of the parent run cancels its harnessAgent child again The change that lets a child go on after any resume dropped the abort listener of the parent run. A stopped run then left its child running, and a run stopped while its child waited never ended. * test(ai-harness): skip four flaky CI tests, and no backoff after the last onModelError retry * feat(ai): export validateJsonSchema and the Standard Schema types * fix(ai-harness): recovery fails an interrupted agent run whose waits never reached the log, so it does not wait forever * fix(ai-harness, ai-mcp, ai-acp, ai-harness-cli): one wait view, and answer and resolve check every wait they end - snapshot().pendingWaits is the one wait view. pendingInterrupts and pendingQuestions are gone. A PendingWait has its interrupt when a chat run stopped for it, turn: true for a wait of the stopped turn, and the secret and url of a question. The HTTP connect payload, the view, MCP, ACP, the CLI, and the e2e apps read it. - The answer to a logged wait is checked with validateJsonSchema of core (cfworker), so pattern and every other keyword count. The hand-written JSON Schema validator and the copied Standard Schema types are gone. - resolve takes only waits with a pending interrupt (use_answer for a step.ask question or a handoff wait), and only the waits of one run (mixed_runs). host.submit checks a resolve against the run of its wait. - A second answer to a wait that an answer ends now is refused. - The MCP resolve tool finds a background-run wait in the log and checks every decision, so it answers a wait that another server opened. * test(e2e): read only this test's request from the aimock journal in the ArkType wire spec Other specs run at the same time against the same aimock. The spec read entries[0], which was a request of a parallel spec, so it got no tool. It now finds its request by the x-test-id header, and it no longer clears the journal that the other specs read. * fix(ai-harness): no dropped-handle warning after host.close() host.close() closes every session, but it kept the hold records. When the garbage collector later dropped a handle that was not closed, the host warned. The warning came at a random time, also while vitest closed a test file, which fails the run with 'Closing rpc while onUserConsoleLog was pending'. The host now gives no warning after close(). A test in client-reads also closes the handle that it opens. * feat(ai): add lastChatMiddleware to SubagentBinding, and the secret_answer credential type - binding.lastChatMiddleware runs after the own middleware of each ctx.chat call of an agent run, so a host middleware sees the final model context and tool results. Nested children get it after their own. - Credential has a secret_answer type with an expiresAt, so the harness keeps a secret answer apart from the user credentials. * fix(ai-harness): keep secrets out of step values, agent middleware, and secret answer rows - step.do throws when its value holds a secret of the session, on the first run and on a replay, so the log never keeps a secret and a tool never gets a silent '[redacted]' value. - The redact, tool result scrub, and tool result recorder middleware of an agent run go in binding.lastChatMiddleware, after the agent's own ctx.chat middleware. The log keeps the result the model got, and an agent middleware that sets the messages again cannot send a secret to the model. - Secret answers are credentials of the type secret_answer with an expiresAt 24 hours later. credentials.list hides that type. An expired ref is refused. - The redactor is a closure. Its walk covers Map and Set, and add() takes each string field of a secret answer with fields. - The duplicate check compares an input without the secrets of the session, as the log keeps it, so a retry with a secret in it is a duplicate. * fix(ai-mcp): ask for a sign-in when the user signs out while the SDK refreshes a token The SDK's new tokens are not saved when credentials.refresh finds no credential, so saveTokens throws AuthRequiredError instead of dropping them with no sign. * test(e2e): run the harness secret answer route against aimock The route uses the OpenAI adapter against aimock with a fixture, and the spec reads the model calls of each host from the aimock journal. * docs(ai-harness): narrow credentials on oauth, so the secret_answer kind type-checks * fix(ai-persistence): keep the harness of the owner or the first offer for a log * fix(ai-harness): check the harness in host.submit, and throw when a child cannot find its root * fix(ai-harness): prune settled state from the thread fold, and change its maps in place * fix(ai-harness): take only JSON values from step.do * fix(ai-harness): back off a failing log in the worker, free the slot of a long sleep, and need media on a shared host * test(ai-harness): skip a build-stub test that times out under load * chore(changeset): note the review fixes of the log session * fix(ai-harness): need media only on a host with ownership, and warn once on another host --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai-harness): refuse an open while a thread purges, and a delete of a thread with no entry on a host with access * fix(ai-harness): throw on each call of a closed handle, and keep the quiet reopen only for a live handle * fix(ai-harness): share a host plugin setup only for the same plugin object * refactor(ai-harness): build the live and the observed snapshot with one builder * fix(ai-harness-cli): resolve at once on a second signal with your own host, also when the turn ignores the cancel * chore(changeset): note the review fixes of the session lifecycle --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…shared sessions, and a run context (#1812) * feat(ai-persistence): session index v2 keyed by tenant and thread, with owner, members, and required compare-and-set * feat(ai-harness): a RunContext for every run, so tools and plugins act as the user who started the run * feat(ai-harness): access types and the membersOnly() policy * feat(ai-harness): tenant-keyed index rows, log ids, and media keys * feat(ai-harness): host access with a principal-bound handle, sharing, revoke, and handlers that fail closed * docs(ai-harness): access and tenants, shared threads on membersOnly, and the run context * feat(ai-mcp, ai-acp, ai-harness-cli): the MCP server, the ACP agent, and the CLI act as a principal * fix(ai-harness): every thread-keyed store write carries the tenant * fix(ai-harness): delete removes the transcript, session plugins see the owner, host.hasAccess, and events wait for a new thread * test(ai-e2e): the protocol route on host access, and a shared-thread access spec * test(ai-harness): delete removes the transcript and closes the session * chore(ai-harness-cli): list @tanstack/ai-mcp as a dev dependency for the /mcp test * feat(ai-harness): session lifecycle with handles, observe, retention, host plugins, and a purging delete (#1813) * fix(ai-harness, ai-mcp): OAuth flows and commands stop on their signal * fix(ai-harness-cli): SIGINT and SIGTERM cancel the turn, then stop, in every mode * fix(ai-harness): one retention policy bounds every per-session collection, the feed, and the log tail * feat(ai-harness, ai-mcp, ai-acp, ai-harness-cli): reference-counted session handles with idle close and maxOpen * docs(ai-harness): lifecycle and retention, handles, observe, signals, and OAuth cancel * feat(ai-harness, ai-mcp, ai-acp): host.observe, a read-only session view, and read routes on it * test(ai-e2e): delete purges media and messages, and reads work after idle close * feat(ai-harness, ai-mcp): host-lifetime plugins, MCP stdio servers once per host, and delete that purges a thread * fix(ai-harness): definePlugin infers its definition only from the argument * fix(ai-harness): key host plugins by the harness definition, not its name * test(ai-mcp): open two sessions of one harness definition to share a stdio server * test(ai-harness, ai-harness-cli): give polled checks 5 s, for slower CI runs * feat(ai-persistence, ai-harness): the session log is the session, with ownership, an input lane, workers, schedules, and snapshots (#1814) * feat(ai-harness): thread-state log records and their fold * feat(ai-persistence, ai-harness): one persistence shape, every host has a log * feat(ai-harness): settings, config, plugin state, interrupted turns, overrides, and agent results live in the log * feat(ai-harness): one ownership epoch per log, renewed once per host, with abort on loss * feat(ai-harness): runs and interrupts live in the session log * feat(ai-persistence, ai-harness): inputs from any machine through the log input lane, host.submit, and no inbox, work claims, or leases * feat(ai-harness): createHarnessWorker, session.schedule, and step.sleep on the log * test(e2e): a worker on another host runs a submitted input and a schedule * feat(ai-harness): one log per session tree, session.logState(), and a wake when a busy session closes * feat(ai-harness): snapshots in the log store by size or time, retention logRecords, and the harness.gap event * fix(ai-harness): export LogOwnedElsewhereError, and say that the log keeps ephemeral messages with the turn * docs(ai-harness, ai-persistence): durable sessions, one log per tree, workers, snapshots, and the changesets for the log as the session * ci: apply automated fixes * fix(ai-harness): check answers and cancels from submit and schedule against their run, a schedule against its maker, and keep a same-host takeover and a lost log's inputs working * test(ai-harness): give polled checks 5 s, for slower CI coverage runs * test(e2e): read the data lines of POST run streams that now carry an id line per event * feat(ai, ai-harness, ai-mcp): one redaction sink, the final tool result in the log, secret answers by reference, and single-flight OAuth refresh (#1815) * feat(ai): CredentialStore compare-and-set pair, readCredentialEntry, and one scrubSecrets * feat(ai-harness): one redactor per session for log records, feed events, plugin state, notes, and the model context * feat(ai-harness): the log keeps the final tool result the model saw, and errors and step results leave the session scrubbed * feat(ai-harness): a secret answer sent to another host goes through the credential store as a one-time reference * test(e2e): a secret answer sent to another host never shows in the transcript, the events, the log, or the model input * feat(ai-harness, ai-mcp): OAuth refresh runs once per host, saves with compare-and-set to the scope it came from, and keeps the client registration * docs(ai, ai-harness, ai-mcp): redaction, secret answers across hosts, the final tool result, OAuth refresh, and the CredentialStore compare-and-set pair * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): durable waits in the log, at the boundary, in background runs, and on every transport (#1817) * feat(ai-harness): the Wait type and waits in the log, with expiry, waitFor, settled({ inputId, signal }), and operator answers to human waits * wip(ai-harness, ai-mcp): PR 7 boundary, coding-approvals, live-waits, background-waits (stopped mid-way, not checked) * feat(ai-harness, ai-mcp): waits at the boundary (permissions, the question tool, step.ask), live MCP and sign-in waits, and background runs that suspend and resume * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): agent runs pause on boundary approvals through SubagentBinding.interrupts, and every transport uses one wait shape * test(ai-mcp): the media result has a waits list * test(e2e): an approval asked on one server is answered on another, and an unanswered wait expires * docs(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): wait for a person, step.ask, approvals at the boundary, background waits, and waits on every transport * fix(ai-harness): a session that does not own the log shows the waits and interrupts other hosts write * fix(ai-harness): lint errors in agent waits, step.ask, and permission resources * test(ai-harness, ai-code-mode): approvals are turn waits answered with resolve, and the device code carries expiresAt * test(e2e): in acceptEdits mode the write asks before it runs, with its formatter, and a reject writes nothing * feat(ai-harness): the agent tree, with typed agent actions, limits and budgets that hold, run inheritance, shared state, durable groups, and harnessAgent as a child run (#1818) * feat(ai-harness): typed subject-first agent actions start, run, and group, with grouped options, for plugins and agent code * feat(ai-harness): the run tree in the log, with RunNode for turns, agents, tools, and commands, and agentRuns read from it * feat(ai-harness): agent limits and budgets that hold, with queued starts, per-run usage, timeouts, attempts, and a budget code * feat(ai-harness): background runs inherit turn plugins, child signals follow their parent, tool-mode children get ctx.harness and share the turn budget, and ctx.harness.state shares state by key * feat(ai-harness): durable agent groups with a concurrency gate and keyed children, and routed agents that take steers, late joins, and turn hooks * docs(ai-harness): agents from code, groups, limits, budgets, the run tree, shared state, and routed agents * feat(ai-harness): harnessAgent runs as a child session in the root log, for the parent's user and keys, with usage, budget, waits, and events in the parent run * docs(ai-harness): the harnessAgent child run, as built * test(e2e): agent groups with a concurrency gate, a run budget, shared state, and a harnessAgent child run * test(ai-harness): give polled checks and a takeover deadline room for slower CI coverage runs * feat(ai-harness): handoffs between agents and to a person, the active agent in the log, and host.transfer between harnesses (#1819) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * test(e2e): the handoff route reads view text by message role * feat(ai-harness, ai-sandbox, ai-sandbox-local-process): coding tools for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting. * feat(ai-harness, ai-persistence): an operator role, so the user who waits for a person cannot act as that person membersOnly() gives the operator action only to the role 'operator' by default. membersOnly({ operator: { roles } }) sets the roles that have it. A human wait takes only 'operator', for an answer and for a resolve that ends it, on a handle and through host.submit. * fix(ai-harness): handoff review fixes - The fold ends a handoff to a person when its human wait settles in any way, and refuses a human handoff record with no waitId. - The router writes the move to the main model once when the active agent is gone. - An operator can hand back to a root agent of a turn plugin: the human handoff record keeps the agents of its turn. - routing.handoffs: false gives no model the handoff tool. - A handoff stops only the chat that made it. - A handoff in a part that also stops for interrupts waits with them, so the resolve turn runs the target or pauses for a person. - The view adds no empty message for a handoff to a person or an operator hand-back. - An operator reply keeps its text in the assistant message and its files in a marked user note, which the view shows with the reply. - host.transfer throws for a source thread that does not exist. * fix(ai): ask onShouldContinue after the resumed tools, before the next model call A resume runs the pending tool calls, then went to the model with no loop check. Now the middleware onShouldContinue decides, as after any tool phase. false ends the run with no more model calls. So a harness handoff in a part that also waited for a client tool runs the target in the resolve turn, and the agent that handed off does not answer again. * fix(ai-harness): a harnessAgent child goes on after any resume, and a POST run resume of its wait streams the parent run The parent run now listens for the next turn of the child session, so an answer that comes early, or a child that resumes by itself (an expired wait), no longer leaves the parent run waiting for good. An answer to a child wait returns a receipt that names the parent run, so the POST run handler streams that run to its end. * fix(ai-harness): a plugin start with parent: ctx.context.run is a child of that agent run A top-level agent run that awaited a plugin start could wait for good under maxConcurrent: the start queued behind the run that awaited it. With parent: ctx.context.run, the start is a child of the running agent run, as ctx.harness.agents starts it, and takes no maxConcurrent slot. * fix(ai-harness): child ids of a resumable run in their own namespaces, so keyed children never collide and an outliving child keeps its steps Keyed and positional children now get separate ids (start:k:<key> vs start:n:<n>, group:k:<key> vs group:n:<n>), after a '/' that the settle cleanup prefix agent:<parentInputId>: cannot match. So a background child that outlives its parent keeps its steps, tool results, and kept answers. A known child of another agent than the one asked for now throws. * fix(ai-harness): a resumed agent run carries the usage of the run before, so its budget holds across a restart The log fold now keeps the usage sum of a run whose input is still applied, and moves it to the next run of that input when it applies. So maxTokens and maxCost count every attempt of a resume: true run. * fix(ai-harness): onModelError of an agent run retries only the failed model call The option was asked for any failure of the run, and 'retry' ran the whole agent again from its first message, with no bound: a tool side effect ran twice, and a permanent error looped for good. Now a chat middleware asks the option only for a model call that fails before it streams, waits a short backoff, and calls that model again. Core calls one model call at most 3 times. * fix(ai-harness): host.open reserves only real harnessAgent child and harnessText inner thread ids host.open refused any thread id that contained /agent/ or /inner/. One helper now refuses such an id only when the part before the marker is a thread of the index, so an app id like org/agent/notes opens. * docs(ai-harness): document only the stable fields of harness.run.node, and mark its bookkeeping fields internal * chore(ai-harness): import the backoff wait under its own name, so it shadows no wait * refactor(ai-harness): one admission gate for maxConcurrent slots, group slots, and tree budget calls The session maxConcurrent slots, the slots of each group, and the tree budget calls that ran when a queued child started were three separate mechanisms. A small Gate now holds the slots of both the session and a group, with FIFO waiters, drop on abort, and an admit hook that takes the tree budget call as a start enters. admitStart replaces reserveWhenReady. No behavior change. * feat(ai-harness): a plugin start in a tool call of an agent run is a child of that run, with parent: null to opt out ctx.agents of a plugin now finds the agent run whose tool call runs (when it is the only operation of the session in a tool call), and starts the agent as its child, so it takes no maxConcurrent slot and the run can await it. parent: { runId } still names the run, and parent: null starts a top-level run that takes a slot. * fix(ai-harness): refuse every /inner/ thread id from app code host.open, host.submit, host.fork, and host.transfer now refuse any thread id with /inner/, also when the thread before it does not exist yet, so no app call can take the id of a harnessText inner session first. Only the throughOuterThread open gets one. The narrow /agent/ rule stays. * fix(ai-harness): a stop of the parent run cancels its harnessAgent child again The change that lets a child go on after any resume dropped the abort listener of the parent run. A stopped run then left its child running, and a run stopped while its child waited never ended. * test(ai-harness): skip four flaky CI tests, and no backoff after the last onModelError retry * feat(ai): export validateJsonSchema and the Standard Schema types * fix(ai-harness): recovery fails an interrupted agent run whose waits never reached the log, so it does not wait forever * fix(ai-harness, ai-mcp, ai-acp, ai-harness-cli): one wait view, and answer and resolve check every wait they end - snapshot().pendingWaits is the one wait view. pendingInterrupts and pendingQuestions are gone. A PendingWait has its interrupt when a chat run stopped for it, turn: true for a wait of the stopped turn, and the secret and url of a question. The HTTP connect payload, the view, MCP, ACP, the CLI, and the e2e apps read it. - The answer to a logged wait is checked with validateJsonSchema of core (cfworker), so pattern and every other keyword count. The hand-written JSON Schema validator and the copied Standard Schema types are gone. - resolve takes only waits with a pending interrupt (use_answer for a step.ask question or a handoff wait), and only the waits of one run (mixed_runs). host.submit checks a resolve against the run of its wait. - A second answer to a wait that an answer ends now is refused. - The MCP resolve tool finds a background-run wait in the log and checks every decision, so it answers a wait that another server opened. * test(e2e): read only this test's request from the aimock journal in the ArkType wire spec Other specs run at the same time against the same aimock. The spec read entries[0], which was a request of a parallel spec, so it got no tool. It now finds its request by the x-test-id header, and it no longer clears the journal that the other specs read. * fix(ai-harness): no dropped-handle warning after host.close() host.close() closes every session, but it kept the hold records. When the garbage collector later dropped a handle that was not closed, the host warned. The warning came at a random time, also while vitest closed a test file, which fails the run with 'Closing rpc while onUserConsoleLog was pending'. The host now gives no warning after close(). A test in client-reads also closes the handle that it opens. * feat(ai): add lastChatMiddleware to SubagentBinding, and the secret_answer credential type - binding.lastChatMiddleware runs after the own middleware of each ctx.chat call of an agent run, so a host middleware sees the final model context and tool results. Nested children get it after their own. - Credential has a secret_answer type with an expiresAt, so the harness keeps a secret answer apart from the user credentials. * fix(ai-harness): keep secrets out of step values, agent middleware, and secret answer rows - step.do throws when its value holds a secret of the session, on the first run and on a replay, so the log never keeps a secret and a tool never gets a silent '[redacted]' value. - The redact, tool result scrub, and tool result recorder middleware of an agent run go in binding.lastChatMiddleware, after the agent's own ctx.chat middleware. The log keeps the result the model got, and an agent middleware that sets the messages again cannot send a secret to the model. - Secret answers are credentials of the type secret_answer with an expiresAt 24 hours later. credentials.list hides that type. An expired ref is refused. - The redactor is a closure. Its walk covers Map and Set, and add() takes each string field of a secret answer with fields. - The duplicate check compares an input without the secrets of the session, as the log keeps it, so a retry with a secret in it is a duplicate. * fix(ai-mcp): ask for a sign-in when the user signs out while the SDK refreshes a token The SDK's new tokens are not saved when credentials.refresh finds no credential, so saveTokens throws AuthRequiredError instead of dropping them with no sign. * test(e2e): run the harness secret answer route against aimock The route uses the OpenAI adapter against aimock with a fixture, and the spec reads the model calls of each host from the aimock journal. * docs(ai-harness): narrow credentials on oauth, so the secret_answer kind type-checks * fix(ai-persistence): keep the harness of the owner or the first offer for a log * fix(ai-harness): check the harness in host.submit, and throw when a child cannot find its root * fix(ai-harness): prune settled state from the thread fold, and change its maps in place * fix(ai-harness): take only JSON values from step.do * fix(ai-harness): back off a failing log in the worker, free the slot of a long sleep, and need media on a shared host * test(ai-harness): skip a build-stub test that times out under load * chore(changeset): note the review fixes of the log session * fix(ai-harness): need media only on a host with ownership, and warn once on another host --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai-harness): refuse an open while a thread purges, and a delete of a thread with no entry on a host with access * fix(ai-harness): throw on each call of a closed handle, and keep the quiet reopen only for a live handle * fix(ai-harness): share a host plugin setup only for the same plugin object * refactor(ai-harness): build the live and the observed snapshot with one builder * fix(ai-harness-cli): resolve at once on a second signal with your own host, also when the turn ignores the cancel * chore(changeset): note the review fixes of the session lifecycle --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai-harness): run the rest of an answered turn as the user who sent it When the owner answers the approval of a member's turn, the resumed turn used the credentials and provider key of the owner. It now runs as the sender of the stopped turn, also after a restart. The log still keeps the answerer as the actor of the resolve. * fix(ai-harness): keep the keys of one user out of the shared provider-keys state The plugin state showed 'env' for a key that a host with access never reads, and /connect wrote the status of one user into the state that every member reads. The state now holds only the env vars that the session reads, and /keys checks the keys of the user who runs it. * fix(ai-harness): explicit access rules, one owner record, and fail-closed child and media checks - access takes { identify, authorize, listFilter?, members?, envKeys? }. membersOnly() returns authorize, listFilter, and members, so spread it into access. The host no longer sniffs a marker on authorize. - The owner is kept once: the member with the role owner. SessionIndexEntry has no ownerId. A share cannot change the role of the owner. - host.open of a child thread checks read on its root first, and a child whose root is missing is refused, so a stranger gets 403 and never the parent thread id. - A signed media URL proves only that the handler made it. The handler runs identify for it and checks read for that user in the tenant of the URL. The fabricated principal is gone. - A revoke of a user in GET host-events drops every kept read decision, so the child threads of the revoked thread stop too. A deleted child is decided through its parent. - createHarnessMcpServer uses requestUsers from the server entry, the same fail-closed rule as the HTTP handler and the socket. - Tests for the delete refusal over HTTP, a revoked handle that cancels its own run, and a member turn that the owner approves. * docs(ai-harness): explicit access rules, members-only ownership, and signed media that needs a user * feat(ai-harness): access.anonymous gives a guest for a signed media URL with no login The handler calls it only on the signed media route, after the signature checks out, and only when identify finds no user. authorize then decides read for the guest, in the tenant of the URL. Without it, the request gets 401. The guest reaches no other route. * refactor(ai-harness)!: nest the access rules in access.policy access is now { identify, policy, anonymous?, envKeys? }, and a policy is { authorize, listFilter?, members? }. membersOnly() returns a policy, so a host passes policy: membersOnly(). A custom policy is policy: { authorize }. No spread of the rules into access. Callers, tests, e2e routes, docs, and the changeset follow. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…, and grouped options (#1809) * chore(ai-acp): pin @agentclientprotocol/sdk to ~1.8.0 * chore: keep the lockfile peer resolution unchanged for the ACP pin * refactor(ai-harness): drop the 'run' lifetime, type schema-less command input as unknown, use core metadata keys * refactor(ai-harness): subpath entry points for server, oauth, media, and Node plugins * refactor(ai-harness): HarnessSession as a type over a private class, whenIdle, and harnessOf * refactor(ai-harness): group host options per concern, and keep project and reduce on the harness * docs(ai-harness): one-object samples, subpath imports, grouped options, and the import guide * refactor(ai-harness): one-object parameters for the host, session, client, view, and helpers * docs(ai-harness): client and view samples in the one-object shapes * refactor(ai-harness): ctx.harness always present, and harness-first host.open and helpers * fix(ai, ai-harness): each context default registration removes only itself, an empty media.secret fails at start, and the docs say the oauth loopback helpers need Node * feat(ai-persistence, ai-harness): identity and access, with tenants, shared sessions, and a run context (#1812) * feat(ai-persistence): session index v2 keyed by tenant and thread, with owner, members, and required compare-and-set * feat(ai-harness): a RunContext for every run, so tools and plugins act as the user who started the run * feat(ai-harness): access types and the membersOnly() policy * feat(ai-harness): tenant-keyed index rows, log ids, and media keys * feat(ai-harness): host access with a principal-bound handle, sharing, revoke, and handlers that fail closed * docs(ai-harness): access and tenants, shared threads on membersOnly, and the run context * feat(ai-mcp, ai-acp, ai-harness-cli): the MCP server, the ACP agent, and the CLI act as a principal * fix(ai-harness): every thread-keyed store write carries the tenant * fix(ai-harness): delete removes the transcript, session plugins see the owner, host.hasAccess, and events wait for a new thread * test(ai-e2e): the protocol route on host access, and a shared-thread access spec * test(ai-harness): delete removes the transcript and closes the session * chore(ai-harness-cli): list @tanstack/ai-mcp as a dev dependency for the /mcp test * feat(ai-harness): session lifecycle with handles, observe, retention, host plugins, and a purging delete (#1813) * fix(ai-harness, ai-mcp): OAuth flows and commands stop on their signal * fix(ai-harness-cli): SIGINT and SIGTERM cancel the turn, then stop, in every mode * fix(ai-harness): one retention policy bounds every per-session collection, the feed, and the log tail * feat(ai-harness, ai-mcp, ai-acp, ai-harness-cli): reference-counted session handles with idle close and maxOpen * docs(ai-harness): lifecycle and retention, handles, observe, signals, and OAuth cancel * feat(ai-harness, ai-mcp, ai-acp): host.observe, a read-only session view, and read routes on it * test(ai-e2e): delete purges media and messages, and reads work after idle close * feat(ai-harness, ai-mcp): host-lifetime plugins, MCP stdio servers once per host, and delete that purges a thread * fix(ai-harness): definePlugin infers its definition only from the argument * fix(ai-harness): key host plugins by the harness definition, not its name * test(ai-mcp): open two sessions of one harness definition to share a stdio server * test(ai-harness, ai-harness-cli): give polled checks 5 s, for slower CI runs * feat(ai-persistence, ai-harness): the session log is the session, with ownership, an input lane, workers, schedules, and snapshots (#1814) * feat(ai-harness): thread-state log records and their fold * feat(ai-persistence, ai-harness): one persistence shape, every host has a log * feat(ai-harness): settings, config, plugin state, interrupted turns, overrides, and agent results live in the log * feat(ai-harness): one ownership epoch per log, renewed once per host, with abort on loss * feat(ai-harness): runs and interrupts live in the session log * feat(ai-persistence, ai-harness): inputs from any machine through the log input lane, host.submit, and no inbox, work claims, or leases * feat(ai-harness): createHarnessWorker, session.schedule, and step.sleep on the log * test(e2e): a worker on another host runs a submitted input and a schedule * feat(ai-harness): one log per session tree, session.logState(), and a wake when a busy session closes * feat(ai-harness): snapshots in the log store by size or time, retention logRecords, and the harness.gap event * fix(ai-harness): export LogOwnedElsewhereError, and say that the log keeps ephemeral messages with the turn * docs(ai-harness, ai-persistence): durable sessions, one log per tree, workers, snapshots, and the changesets for the log as the session * ci: apply automated fixes * fix(ai-harness): check answers and cancels from submit and schedule against their run, a schedule against its maker, and keep a same-host takeover and a lost log's inputs working * test(ai-harness): give polled checks 5 s, for slower CI coverage runs * test(e2e): read the data lines of POST run streams that now carry an id line per event * feat(ai, ai-harness, ai-mcp): one redaction sink, the final tool result in the log, secret answers by reference, and single-flight OAuth refresh (#1815) * feat(ai): CredentialStore compare-and-set pair, readCredentialEntry, and one scrubSecrets * feat(ai-harness): one redactor per session for log records, feed events, plugin state, notes, and the model context * feat(ai-harness): the log keeps the final tool result the model saw, and errors and step results leave the session scrubbed * feat(ai-harness): a secret answer sent to another host goes through the credential store as a one-time reference * test(e2e): a secret answer sent to another host never shows in the transcript, the events, the log, or the model input * feat(ai-harness, ai-mcp): OAuth refresh runs once per host, saves with compare-and-set to the scope it came from, and keeps the client registration * docs(ai, ai-harness, ai-mcp): redaction, secret answers across hosts, the final tool result, OAuth refresh, and the CredentialStore compare-and-set pair * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): durable waits in the log, at the boundary, in background runs, and on every transport (#1817) * feat(ai-harness): the Wait type and waits in the log, with expiry, waitFor, settled({ inputId, signal }), and operator answers to human waits * wip(ai-harness, ai-mcp): PR 7 boundary, coding-approvals, live-waits, background-waits (stopped mid-way, not checked) * feat(ai-harness, ai-mcp): waits at the boundary (permissions, the question tool, step.ask), live MCP and sign-in waits, and background runs that suspend and resume * feat(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): agent runs pause on boundary approvals through SubagentBinding.interrupts, and every transport uses one wait shape * test(ai-mcp): the media result has a waits list * test(e2e): an approval asked on one server is answered on another, and an unanswered wait expires * docs(ai, ai-harness, ai-mcp, ai-acp, ai-harness-cli): wait for a person, step.ask, approvals at the boundary, background waits, and waits on every transport * fix(ai-harness): a session that does not own the log shows the waits and interrupts other hosts write * fix(ai-harness): lint errors in agent waits, step.ask, and permission resources * test(ai-harness, ai-code-mode): approvals are turn waits answered with resolve, and the device code carries expiresAt * test(e2e): in acceptEdits mode the write asks before it runs, with its formatter, and a reject writes nothing * feat(ai-harness): the agent tree, with typed agent actions, limits and budgets that hold, run inheritance, shared state, durable groups, and harnessAgent as a child run (#1818) * feat(ai-harness): typed subject-first agent actions start, run, and group, with grouped options, for plugins and agent code * feat(ai-harness): the run tree in the log, with RunNode for turns, agents, tools, and commands, and agentRuns read from it * feat(ai-harness): agent limits and budgets that hold, with queued starts, per-run usage, timeouts, attempts, and a budget code * feat(ai-harness): background runs inherit turn plugins, child signals follow their parent, tool-mode children get ctx.harness and share the turn budget, and ctx.harness.state shares state by key * feat(ai-harness): durable agent groups with a concurrency gate and keyed children, and routed agents that take steers, late joins, and turn hooks * docs(ai-harness): agents from code, groups, limits, budgets, the run tree, shared state, and routed agents * feat(ai-harness): harnessAgent runs as a child session in the root log, for the parent's user and keys, with usage, budget, waits, and events in the parent run * docs(ai-harness): the harnessAgent child run, as built * test(e2e): agent groups with a concurrency gate, a run budget, shared state, and a harnessAgent child run * test(ai-harness): give polled checks and a takeover deadline room for slower CI coverage runs * feat(ai-harness): handoffs between agents and to a person, the active agent in the log, and host.transfer between harnesses (#1819) * wip(ai-harness): PR 9 transfer (stopped mid-way, not checked) * feat(ai-harness): host.transfer moves a conversation to another harness with an envelope, links both threads, and is safe to retry; fork stays in its harness * feat(ai-harness): the active agent lives in the log and answers when the router picks nothing; routing.strategy 'handoff' becomes routing.after 'main' * feat(ai-harness): ctx.harness.handoff, a handoff tool, and one handoff path for code, tool, and router, with the target answering in the same turn * feat(ai-harness): hand a turn to a person through a human wait, operator replies with as: 'operator', and run.handoff for the main model's tools * feat(ai-harness): handing a turn to a person is opt-in with routing.humans, and an operator reply shows live in the view * test(e2e): handoffs to an agent and to a person, and a transfer between harnesses that is safe to retry * docs(ai-harness): hand off a conversation to an agent, to a person, or to another harness * test(e2e): the handoff route reads view text by message role * feat(ai-harness, ai-sandbox, ai-sandbox-local-process): coding tools for many threads, with workspaces per run, locks, bounded jobs, and moving threads between hosts (#1820) * fix(ai-harness, ai-sandbox): coding commands get an env allowlist, one workspace per thread, and real paths checked under the lock * wip: unfinished node work, saved before a shutdown (not reviewed) * fix(ai-harness): shared file locks with an unchanged check, and snapshots per thread and per run * fix(ai-harness, ai-sandbox, ai-sandbox-local-process): bounded background jobs with a ring buffer, a kill that waits for the spawn, workspace close, and an env allowlist in the local sandbox * perf(ai-harness, ai-sandbox): size caps for webfetch and read_file, range reads in the sandbox, one grep listing, and coding events for jobs, snapshots, and the formatter * docs(ai-harness, ai-sandbox, ai-sandbox-local-process): run the coding tools for many threads, with workspaces per thread, jobs, moving threads between hosts, and snapshots in the log * feat(ai-harness): ownership.hostId pins threads that use a local folder to their host, with E2E specs for env, roots, moving a sandbox thread, stranded, and pinning * chore(ai-harness): allow the ps binary in knip, and give polled checks room for slower CI coverage runs * feat(ai, ai-harness): protocol v2 with one wire encoding, healthy streams, run tags, lifecycle events, usage by source, and tracing (#1821) * feat(ai-harness, ai): protocol v2 frames with error codes and request ids, observability types, and pull-based SSE and socket senders * docs(ai-harness): first draft of the harness protocol page * feat(ai-harness): one wire encoding on every transport with hello, version, request ids, and heartbeats; hooks and status from other hosts; a gap event for evicted feeds * test(e2e): the access spec skips the hello frame of the session stream * feat(ai-harness): a grouped stream option for the heartbeat, the slow-consumer close, and the SSE and socket buffer limits * feat(ai-harness): run tags on every event frame, ctx.model.chat for plugins, and usage by source * feat(ai-harness): lifecycle events, counted router and transfer model calls, tracing with span links, and onOperation reports * docs(ai-harness): the harness protocol page, keeping the stream open, stopping a turn from useChat, and the changesets for reporting and observability * docs(ai-harness): run tags and the gap reload in the custom UI page * test(e2e): the wire, the gap, the run tree in one stream, lifecycle events, tracing, remote status, cancel from another host, and an unsupported version * test(ai-harness): give polled checks room for slower CI coverage runs * feat(ai-mcp, ai-acp, ai-code-mode, ai-skills, ai-harness-cli, ai-harness): integrations on the new harness, with MCP reconnects, ACP order, a skills watcher per host, and a CLI that cancels at once (#1822) * feat(ai-mcp): mcp() reconnects with backoff, lists tools again on list_changed, and takes needsApproval and toolName per server * fix(ai-acp, ai-code-mode, ai-harness): ACP keeps prompt order and releases idle sessions, code mode never moves approval tools, harnessText acts for the caller * perf(ai-skills): one folder watcher per host with a cache of the skills * feat(ai-harness-cli): cancel and steer act at once in a terminal, loadOptional reports real errors, and --dashboard goes away * chore(changeset): the integration changes for mcp, acp, code mode, skills, and harnessText * perf(ai-harness, ai-harness-cli): cheaper plugin, log, media, view, client, and host-event paths, no idle store calls, and context prompts after the history (#1823) * perf(ai-harness): cache plugin lists and permission patterns, read instruction files async, parse SSE as a stream with one abort listener and capped backoff, and give each control input an id * perf(ai-harness): stream media, cache base64 parts across turns, cap open-ended ranges, and add a byte-capped event ring * perf(ai-harness, ai-harness-cli): the view and the printer only redo what changed, children load on demand with view.loadAgent, operation.stream reads from its start * perf(ai-harness): ring buffer log tail, joins and rebuilt operations read from the run start, a per-tenant host event bus, no MESSAGES_SNAPSHOT in the log, and no idle store calls * perf(ai-harness): plugin prompts with placement 'context' go after the history, so the cached prompt prefix stays valid * test(ai-harness): one threadId in the expected gap frame * test(e2e): the first-party fixtures match the context prompt that now comes after the user message * fix(ai, ai-harness): a guard tier for tool calls, complete plugin removal, media and body limits, signing keys, and turn and config fixes (#1824) * fix(ai-harness): a settled operation keeps its status, a media signer with key rotation, and agents() rule docs that match the code * fix(ai-harness): every plugin contribution has an owner, ctx.on cleans up, inline media goes to the media store, and capabilities report the socket only when mounted * docs(ai-harness): say when capabilities report the WebSocket * feat(ai): onGuardToolCall middleware hook that runs on the final arguments of every tool call * test(e2e): onGuardToolCall denies on the final args after a transformArgs * fix(ai-harness): tool turns take no steers, a turn keeps its config, removing a plugin removes all of it, body and frame limits, signing keys, and a warning on tool name clashes * fix(ai-harness): the media signer's key check covers an empty secret * fix(ai-harness-cli): serve signs media URLs with a key derived from the token, so a short token meets the 32-byte key rule * feat(ai-harness, ai-mcp, ai-acp, ai-code-mode, ai-harness-cli): the final review of the harness, with its fixes, one object per call, and the known limits (#1825) * fix(ai-harness, ai-mcp, ai-acp, ai-code-mode): final review fixes for durability, access, plugins, transports, integrations, and coding tools * fix(ai-acp): one agent app serves one connection, with editor capabilities read from that connection, and no private SDK field * fix(ai-harness): a thread delete ends it in first-party plugins, the bash spill is scrubbed, removed schedules go at once, agent run receipts resolve, and session.append takes one object with an optional inputId * refactor(ai-harness, ai-mcp, ai-harness-cli): session, client, and view methods take one object, the client and view use putMedia and client.sessions, and internal fields leave the public types * docs(ai-harness, ai-mcp, ai-acp, ai-code-mode): the final review changes, the fixed durable-tools and server-tools text, and a known limits section * fix(ai-harness): revoke and unshare reject when work does not stop, thread delete reaches child logs and known harnesses, plugins declare kept records, and the review fixes - host.revoke and host.sessions.unshare reject when the work stop fails (the access is gone already). unshare ends the reads before it stops the work. - createHarnessHost takes harnesses, so a delete on a new host ends the thread in its plugins. An unknown harness warns. A child thread ends in the log of its root. - A plugin declares keepRecords. The core fold reads them from the plugins of the harness that folds the log, and counts per type. snapshots() keeps its 500 steps this way. - revert and unrevert open the workspace of the thread from the log when no turn opened it on this host. - The harness.revert and harness.job records keep the principal. - append with no inputId goes through accept(), so its id is real. - A formatter config below the root, or a write under node_modules, asks again. - The base64 cache takes the largest encodedBytes. The client takes idleTimeoutMs. - An events() read listens for a revoke from the call, not the first pull. * docs(ai-harness): the harnesses option, keepRecords, idleTimeoutMs, a revoke that rejects, and one known limit less * feat(ai-harness): ctx.session.keptRecords reads the kept records of a plugin, and session.revert reads them the same way * fix(ai-harness): an always answer saves no rule for an @unsaved: action * refactor(ai-harness): mounted plugins keep each contribution list with its owner, not in parallel owner arrays * fix(ai-harness): a plugins remove setting is rejected when a kept turn plugin or a harness middleware needs the removed capability * fix(ai-harness): client media checks every part before it stores any, refuses provider file handles unless media.allowProviderFiles, and the frame limit docs name the server maxPayload * fix(ai-harness): a retried client input with inline media reuses the stored media, so it stores nothing again and is a duplicate, not a conflict * docs(ai-harness): routed root-agent turns take steers and late joins, only session.tool() turns do not * fix(ai-harness-cli): --serve signs media URLs with --media-key or HARNESS_MEDIA_KEY, or a random key, never with the bearer token * fix(ai-harness): keep the usage source in the view, and a host events read that listens from the call * fix(ai-harness): send context prompts as the last system prompt, not as a user message * fix(ai-harness): check the snapshot shape in client.events(), and document its head-first default * feat(ai-harness): an onTurnStart plugin hook, used by projectInstructions and workspaceTools * fix(ai-harness): keep the interrupt messages snapshot in the log, so a rejoin from the log gets it * fix(ai-harness-cli): load the saved child agents in the TUI example, and document view.loadAgent for a ui * fix(ai-mcp): no reconnect timer after close, backoff that survives crash loops, and onDuplicateTool 'error' * fix(ai-harness-cli): tell a missing optional package by its resolve error code, not the message text * fix(ai-harness): check the inner session of harnessText through the outer thread, and pass the run principal on as is * fix(ai-harness): bind the harnessText inner thread to its outer thread before the outer check applies * fix(ai-acp): keep the sessions of the connection known on a host with no session index * refactor(ai-skills): one shared watcher per host that only triggers a sync, with no skill cache * fix(ai-harness): one harnessText inner thread id shape, <outer>/inner/<harness>, on every host * fix(ai-harness): use the real OpenTelemetry types, guard a tracer that throws, and trace a transfer inside it @opentelemetry/api is now an optional peer (type imports, and a lazy load for trace.setSpan when a host has a tracer). The copied span types and the private context key are gone. A tracer or span call that throws is logged and fails no run. The harness.transfer span is part of host.transfer, not a patch after it. * fix(ai-harness): trim the protocol to one version check, put the parent run on RUN_STARTED, and surface harness.error in the client - A requested version other than 2 gets unsupported_version from one equality check. negotiateVersion, SUPPORTED_PROTOCOL_VERSIONS, and the supported field are gone. - harness.event frames and session events no longer carry parentRunId. The AG-UI RUN_STARTED of a child agent run has it. - client.events() and client.hostEvents() connect again after slow_consumer and throw a HarnessProtocolError (code + message) for any other error frame. * fix(ai-harness): no usage event after a run finished, and bySource on old snapshots - A ctx.model.chat call that ends after its run settled (the title plugin) writes the usage record only: the run sends nothing after RUN_FINISHED. - Usage totals from a snapshot written before bySource get an empty map at load, so session.usage().bySource is always an object. * fix(ai-harness, ai-persistence): read takeover and open questions from real state, and clear the runs of a stopped owner - LogStore.acquire returns expired: true when it takes a log from another owner whose ownership expired with no release. harness.ownership.changed says taken_over from that, not from pending inputs. - The host status reads open questions from the folded log only. - The status rule drops the running operations of a stopped owner on taken_over, and the old operation of a resumed input, so a reader host does not show running forever after a takeover. * test(ai-harness): an append is an input, so a non-owner forwards it and the log has its input records - shared-log: check that the two app records land in one store append, or neither, among the input records that the append now writes. - ownership: the append of a host that does not own the log is queued and the owner writes it. A write that is not an input (a schedule) is still refused. - docs: an append on a host that does not own the thread gets a queued receipt. * fix(ai-harness): coding tools review fixes for many threads - The changed-since-read check keeps what each chat run read, so a lead and its tool-mode subagents do not lose each other's edits. - File locks are keyed by the sandbox (or host) and the real path, not by the root string. - A foreground command moves to the background only under maxJobs, and the log gets its process id (exec onStart), so a later host can stop it. - A job that the backend cannot check (killJob throws, or no killJob) stays in the log, and the model hears that it may still run. hostBackend.killJob throws when ps gives no start time. - workspaceRoot(base, ...parts) encodes each part of a workspace folder, so a thread id from a client cannot leave base. - host.unpin({ harness, threadId }) unpins a thread from a host that does not come back. * fix(ai-sandbox, ai-sandbox-local-process): job ids with a start time, one commandEnv, and passEnv - sandboxWorkspaceBackend gives a job id of pid and start time (ps -o lstart), and killJob stops only the process that started then. It rejects when it cannot check a process that runs. - commandEnv and createOutputTail live in @tanstack/ai-sandbox, and local-process imports commandEnv from there. - localProcessSandbox({ passEnv }) gives commands more variables of the host, for a CLI login or a proxy. * docs(ai-harness, ai-sandbox): safe workspace roots, host.unpin, a stable hostId, no isolation claims for host commands, and passEnv * refactor(ai-utils, ai-harness, ai-sandbox, ai-sandbox-local-process): one commandEnv and createOutputTail in @tanstack/ai-utils The three packages import both helpers from @tanstack/ai-utils, a new direct dependency of each. No more copies. commandEnv reads process.env only when there is a process, and createOutputTail needs no Buffer. * feat(ai-persistence, ai-harness): host heartbeats, so a pin lapses only when its host stopped - LogStore.heartbeat({ hostId, ttlMs }). acquire and listOrphaned ignore the affinity of a host with no live heartbeat. The memory store, the conformance suite, and the SQL recipe have it. - A host with ownership.hostId sends a heartbeat each renewMs, also when it owns no log. A live host keeps its pins on idle threads. When it stops, another host reattaches its threads. - Docs for the workspaces, deploy, durable sessions, and store pages. * fix(ai-utils): type the process env of commandEnv, so the React Native smoke typechecks Without process, commandEnv reads {}. The union of process.env and {} cannot be indexed by a string under the React Native smoke tsconfig (TS7053 at command.ts:46), and the Test job stopped there. * fix(ai-vertex): give ai-vertex the same @google/genai copy as ai-gemini again The lockfile moved the @google/genai devDependency of ai-vertex to the zod@4.6.5 peer copy, while ai-gemini keeps the zod@4.3.6 copy. Then the vi.mock('@google/genai') of the vertex tests mocks another file than the one that ai-gemini imports, and tests/factories.test.ts fails. * test(ai-harness): give the live-host pin test a 1 s heartbeat With a 300 ms ttl and a 100 ms heartbeat, a CI run that holds the event loop for 200 ms lets the heartbeat expire, and the pin stops counting. * feat(ai-harness, ai-persistence): an operator role, so the user who waits for a person cannot act as that person membersOnly() gives the operator action only to the role 'operator' by default. membersOnly({ operator: { roles } }) sets the roles that have it. A human wait takes only 'operator', for an answer and for a resolve that ends it, on a handle and through host.submit. * fix(ai-harness): handoff review fixes - The fold ends a handoff to a person when its human wait settles in any way, and refuses a human handoff record with no waitId. - The router writes the move to the main model once when the active agent is gone. - An operator can hand back to a root agent of a turn plugin: the human handoff record keeps the agents of its turn. - routing.handoffs: false gives no model the handoff tool. - A handoff stops only the chat that made it. - A handoff in a part that also stops for interrupts waits with them, so the resolve turn runs the target or pauses for a person. - The view adds no empty message for a handoff to a person or an operator hand-back. - An operator reply keeps its text in the assistant message and its files in a marked user note, which the view shows with the reply. - host.transfer throws for a source thread that does not exist. * fix(ai): ask onShouldContinue after the resumed tools, before the next model call A resume runs the pending tool calls, then went to the model with no loop check. Now the middleware onShouldContinue decides, as after any tool phase. false ends the run with no more model calls. So a harness handoff in a part that also waited for a client tool runs the target in the resolve turn, and the agent that handed off does not answer again. * fix(ai-harness): a harnessAgent child goes on after any resume, and a POST run resume of its wait streams the parent run The parent run now listens for the next turn of the child session, so an answer that comes early, or a child that resumes by itself (an expired wait), no longer leaves the parent run waiting for good. An answer to a child wait returns a receipt that names the parent run, so the POST run handler streams that run to its end. * fix(ai-harness): a plugin start with parent: ctx.context.run is a child of that agent run A top-level agent run that awaited a plugin start could wait for good under maxConcurrent: the start queued behind the run that awaited it. With parent: ctx.context.run, the start is a child of the running agent run, as ctx.harness.agents starts it, and takes no maxConcurrent slot. * fix(ai-harness): child ids of a resumable run in their own namespaces, so keyed children never collide and an outliving child keeps its steps Keyed and positional children now get separate ids (start:k:<key> vs start:n:<n>, group:k:<key> vs group:n:<n>), after a '/' that the settle cleanup prefix agent:<parentInputId>: cannot match. So a background child that outlives its parent keeps its steps, tool results, and kept answers. A known child of another agent than the one asked for now throws. * fix(ai-harness): a resumed agent run carries the usage of the run before, so its budget holds across a restart The log fold now keeps the usage sum of a run whose input is still applied, and moves it to the next run of that input when it applies. So maxTokens and maxCost count every attempt of a resume: true run. * fix(ai-harness): onModelError of an agent run retries only the failed model call The option was asked for any failure of the run, and 'retry' ran the whole agent again from its first message, with no bound: a tool side effect ran twice, and a permanent error looped for good. Now a chat middleware asks the option only for a model call that fails before it streams, waits a short backoff, and calls that model again. Core calls one model call at most 3 times. * fix(ai-harness): host.open reserves only real harnessAgent child and harnessText inner thread ids host.open refused any thread id that contained /agent/ or /inner/. One helper now refuses such an id only when the part before the marker is a thread of the index, so an app id like org/agent/notes opens. * docs(ai-harness): document only the stable fields of harness.run.node, and mark its bookkeeping fields internal * chore(ai-harness): import the backoff wait under its own name, so it shadows no wait * refactor(ai-harness): one admission gate for maxConcurrent slots, group slots, and tree budget calls The session maxConcurrent slots, the slots of each group, and the tree budget calls that ran when a queued child started were three separate mechanisms. A small Gate now holds the slots of both the session and a group, with FIFO waiters, drop on abort, and an admit hook that takes the tree budget call as a start enters. admitStart replaces reserveWhenReady. No behavior change. * feat(ai-harness): a plugin start in a tool call of an agent run is a child of that run, with parent: null to opt out ctx.agents of a plugin now finds the agent run whose tool call runs (when it is the only operation of the session in a tool call), and starts the agent as its child, so it takes no maxConcurrent slot and the run can await it. parent: { runId } still names the run, and parent: null starts a top-level run that takes a slot. * fix(ai-harness): refuse every /inner/ thread id from app code host.open, host.submit, host.fork, and host.transfer now refuse any thread id with /inner/, also when the thread before it does not exist yet, so no app call can take the id of a harnessText inner session first. Only the throughOuterThread open gets one. The narrow /agent/ rule stays. * fix(ai-harness): a stop of the parent run cancels its harnessAgent child again The change that lets a child go on after any resume dropped the abort listener of the parent run. A stopped run then left its child running, and a run stopped while its child waited never ended. * test(ai-harness): skip four flaky CI tests, and no backoff after the last onModelError retry * feat(ai): export validateJsonSchema and the Standard Schema types * fix(ai-harness): recovery fails an interrupted agent run whose waits never reached the log, so it does not wait forever * fix(ai-harness, ai-mcp, ai-acp, ai-harness-cli): one wait view, and answer and resolve check every wait they end - snapshot().pendingWaits is the one wait view. pendingInterrupts and pendingQuestions are gone. A PendingWait has its interrupt when a chat run stopped for it, turn: true for a wait of the stopped turn, and the secret and url of a question. The HTTP connect payload, the view, MCP, ACP, the CLI, and the e2e apps read it. - The answer to a logged wait is checked with validateJsonSchema of core (cfworker), so pattern and every other keyword count. The hand-written JSON Schema validator and the copied Standard Schema types are gone. - resolve takes only waits with a pending interrupt (use_answer for a step.ask question or a handoff wait), and only the waits of one run (mixed_runs). host.submit checks a resolve against the run of its wait. - A second answer to a wait that an answer ends now is refused. - The MCP resolve tool finds a background-run wait in the log and checks every decision, so it answers a wait that another server opened. * test(e2e): read only this test's request from the aimock journal in the ArkType wire spec Other specs run at the same time against the same aimock. The spec read entries[0], which was a request of a parallel spec, so it got no tool. It now finds its request by the x-test-id header, and it no longer clears the journal that the other specs read. * fix(ai-harness): no dropped-handle warning after host.close() host.close() closes every session, but it kept the hold records. When the garbage collector later dropped a handle that was not closed, the host warned. The warning came at a random time, also while vitest closed a test file, which fails the run with 'Closing rpc while onUserConsoleLog was pending'. The host now gives no warning after close(). A test in client-reads also closes the handle that it opens. * feat(ai): add lastChatMiddleware to SubagentBinding, and the secret_answer credential type - binding.lastChatMiddleware runs after the own middleware of each ctx.chat call of an agent run, so a host middleware sees the final model context and tool results. Nested children get it after their own. - Credential has a secret_answer type with an expiresAt, so the harness keeps a secret answer apart from the user credentials. * fix(ai-harness): keep secrets out of step values, agent middleware, and secret answer rows - step.do throws when its value holds a secret of the session, on the first run and on a replay, so the log never keeps a secret and a tool never gets a silent '[redacted]' value. - The redact, tool result scrub, and tool result recorder middleware of an agent run go in binding.lastChatMiddleware, after the agent's own ctx.chat middleware. The log keeps the result the model got, and an agent middleware that sets the messages again cannot send a secret to the model. - Secret answers are credentials of the type secret_answer with an expiresAt 24 hours later. credentials.list hides that type. An expired ref is refused. - The redactor is a closure. Its walk covers Map and Set, and add() takes each string field of a secret answer with fields. - The duplicate check compares an input without the secrets of the session, as the log keeps it, so a retry with a secret in it is a duplicate. * fix(ai-mcp): ask for a sign-in when the user signs out while the SDK refreshes a token The SDK's new tokens are not saved when credentials.refresh finds no credential, so saveTokens throws AuthRequiredError instead of dropping them with no sign. * test(e2e): run the harness secret answer route against aimock The route uses the OpenAI adapter against aimock with a fixture, and the spec reads the model calls of each host from the aimock journal. * docs(ai-harness): narrow credentials on oauth, so the secret_answer kind type-checks * fix(ai-persistence): keep the harness of the owner or the first offer for a log * fix(ai-harness): check the harness in host.submit, and throw when a child cannot find its root * fix(ai-harness): prune settled state from the thread fold, and change its maps in place * fix(ai-harness): take only JSON values from step.do * fix(ai-harness): back off a failing log in the worker, free the slot of a long sleep, and need media on a shared host * test(ai-harness): skip a build-stub test that times out under load * chore(changeset): note the review fixes of the log session * fix(ai-harness): need media only on a host with ownership, and warn once on another host --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai-harness): refuse an open while a thread purges, and a delete of a thread with no entry on a host with access * fix(ai-harness): throw on each call of a closed handle, and keep the quiet reopen only for a live handle * fix(ai-harness): share a host plugin setup only for the same plugin object * refactor(ai-harness): build the live and the observed snapshot with one builder * fix(ai-harness-cli): resolve at once on a second signal with your own host, also when the turn ignores the cancel * chore(changeset): note the review fixes of the session lifecycle --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai-harness): run the rest of an answered turn as the user who sent it When the owner answers the approval of a member's turn, the resumed turn used the credentials and provider key of the owner. It now runs as the sender of the stopped turn, also after a restart. The log still keeps the answerer as the actor of the resolve. * fix(ai-harness): keep the keys of one user out of the shared provider-keys state The plugin state showed 'env' for a key that a host with access never reads, and /connect wrote the status of one user into the state that every member reads. The state now holds only the env vars that the session reads, and /keys checks the keys of the user who runs it. * fix(ai-harness): explicit access rules, one owner record, and fail-closed child and media checks - access takes { identify, authorize, listFilter?, members?, envKeys? }. membersOnly() returns authorize, listFilter, and members, so spread it into access. The host no longer sniffs a marker on authorize. - The owner is kept once: the member with the role owner. SessionIndexEntry has no ownerId. A share cannot change the role of the owner. - host.open of a child thread checks read on its root first, and a child whose root is missing is refused, so a stranger gets 403 and never the parent thread id. - A signed media URL proves only that the handler made it. The handler runs identify for it and checks read for that user in the tenant of the URL. The fabricated principal is gone. - A revoke of a user in GET host-events drops every kept read decision, so the child threads of the revoked thread stop too. A deleted child is decided through its parent. - createHarnessMcpServer uses requestUsers from the server entry, the same fail-closed rule as the HTTP handler and the socket. - Tests for the delete refusal over HTTP, a revoked handle that cancels its own run, and a member turn that the owner approves. * docs(ai-harness): explicit access rules, members-only ownership, and signed media that needs a user * feat(ai-harness): access.anonymous gives a guest for a signed media URL with no login The handler calls it only on the signed media route, after the signature checks out, and only when identify finds no user. authorize then decides read for the guest, in the tenant of the URL. Without it, the request gets 401. The guest reaches no other route. * refactor(ai-harness)!: nest the access rules in access.policy access is now { identify, policy, anonymous?, envKeys? }, and a policy is { authorize, listFilter?, members? }. membersOnly() returns a policy, so a host passes policy: membersOnly(). A custom policy is policy: { authorize }. No spread of the rules into access. Callers, tests, e2e routes, docs, and the changeset follow. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ai,ai-harness): optional ctx.harness, an error for a mixed log fold, and an allow-listed HarnessSession - Remove registerSubagentContextDefaults, its process-global Set, and the harness module side effect that filled it. ctx.harness is now optional on SubagentContextExtension. Every session path still passes it through binding.extend. ai-harness goes back to "sideEffects": false. - A harness with another durability.project or durability.reduce than the one that folds an open log now throws when it opens that log, in place of a silent first-opener-wins fold. - HarnessSession lists its public members (an allow-list), and SessionImpl implements it. takeOver, scrubError, loggedRun, and scheduleRun leave the public type. - The POST run route calls resolveForRun and sendForRun directly, and throws for a session of another package copy. - Fix the expose.commands JSDoc to client.command({ name }). --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This PR finishes the harness redesign with a final review of the whole stack against the eight rules (Closed, Fast, Portable, Isolated, Durable waits, Agent tree, No leaks, Visible). It fixes what the review found, gives session, client, and view methods one object argument, and documents the limits that stay. The fixed bugs are in code from PRs 1 to 14 of this stack, which is not on
main, so this is the last feat step of the stack and not a fix ofmain. This is PR 15 of the harness redesign stack for #1755. It targets #1824 (feat/harness-point-bugs).🎯 Changes
settled()ends when the owner on another host refuses an input.isIdle()counts background jobs.renewMsmust be less thanttlMs(else aRangeError). The default is a third ofttlMs.harness.offer.rejectedrecord. A remote fold reloads settings, config, and the interrupted turn.handle.agents.runusesprincipal.revokeandunsharestop work on any host and block schedules.revokenow returns a Promise.sendandnotetakeprincipal?, with the owner as the default.permissions()decides inonGuardToolCall.decline. A failed first connect is tried again. The OAuth connector reconnects.toolCallId.include. Snapshots and the formatter use the workspace of the thread.inputId.client.events()reconnects after 60 s with no data.cancelInput,revert,background,getMedia,mediaUrl,agentTranscript, andappend.appendtakes an optionalinputIdand returns aReceipt.uploadis nowputMedia, and thread methods move toclient.sessions. View:approve,reject,notice, andputMedia.send(text, options?),steer, andfollowUp. Internal fields leave the public types.Docs: about 27 pages under
docs/harness/, plusdocs/tools/server-tools.mdanddurable-tools.md. A new## Known limitssection indocs/harness/deploy.mdlists what stays open, andoverview.mdlinks to it. The changesets are edited in place.Decisions made while building (Alem's answers hold):
session.appendtakes an optionalinputIdto drop a repeated append.onConnect.harnessAgentchild keeps its waits in memory, the snapshot is the full fold,sessionOfTurnopens threads one at a time, the MCP SDK refreshes once per session, and plugin tools do not reach background agents.✅ Checklist
pnpm run test:pr, or these tests do not apply to this pull request.docs/for this change, or this change is not user-facing.pnpm changeset), or this PR does not change a published package.🚀 Release Impact
Testing
Commands run:
test:typesofai,ai-harness,ai-mcp,ai-acp,ai-harness-cli,ai-code-mode,ai-skills,ai-sandbox, the E2E app, andexamples/harness-cli. All pass.vitest relatedon every changedai-harnesssource file, with--maxWorkers=2: 150 files, 2,006 tests, all pass. The changed test files inai-mcp(9),ai-acp(4),ai-code-mode(1), andai-harness-cli(5) pass too.test:oxlintofai-harness,test:knip,pnpm test:docs,kiira checkon the changed pages, andoxfmt --checkon every changed file.Not run locally: E2E and coverage. CI runs them.
Manual test:
pnpm --dir packages/ai-harness exec vitest run tests/access-work.test.ts tests/ownership.test.ts tests/durable-inputs.test.ts.pnpm --dir packages/ai-mcp exec vitest run tests/connector-reconnect.test.ts tests/harness-elicit.test.ts.tests/access-work.test.ts.How this PR makes testing easy: each fix has a test that failed on the old code. The new files are
access-workand the MCPconnector-reconnect. The other fixes add cases to the ownership, durable-inputs, permissions, snapshots, view, client, and ACP tests. The E2E coding-tools route moves to the one-object API.Risk / rollback
Medium. The one-object API changes call sites of session, client, and view methods, and
revokeis now async. The harness is not released yet, and all callers in this repo are moved. To undo, revert this PR.Public API change
Before
After
Review fixes
These fixes come from a stack review. They are squash-merged into #1824, together with this PR.
createHarnessHost({ harnesses })lists the harnesses of the host, sohost.sessions.deletecan end plugins after a restart. If a harness is still unknown, the delete warns.host.revokeandunsharereject when the work stop fails.unsharecuts reads first.keepRecords, and read them withctx.session.keptRecords(type). The core fold no longer names the snapshots plugin.harness.revertkeeps the principal.appendwithoutinputIdgoes throughaccept(), so the id is real.idleTimeoutMs(default 60 s). The media cache uses the largest cap that is asked for.node_modules/. These answers save no rule.Checks: the unit tests for the changed files, plus
test:typesandtest:oxlintforai-harness. CI runs the rest.