Skip to content

fix(ai-harness): a forwarded command ends with the owner's result or error - #1872

Merged
tombeckenham merged 3 commits into
feat/harness-corefrom
1796-ai-harness-a-forwarded-command-completes-with-no-result-also-when-the-owner-rejects-it
Oct 22, 2026
Merged

tombeckenham merged 3 commits into
feat/harness-corefrom
1796-ai-harness-a-forwarded-command-completes-with-no-result-also-when-the-owner-rejects-it

Conversation

@tombeckenham

@tombeckenham tombeckenham commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

When a durable host forwards session.runCommand(...) to the host that owns the thread, the caller got completed with undefined. This was true also when the owner refused the command or the command threw. Now the forwarding host waits for the owner to settle the input, and ends the operation the same way: with the result, with the error, or with InputRejectedError.

🎯 Changes

  • executeCommand in packages/ai-harness/src/session/turn.ts: an offered admission now goes to a new followCommand. It reads the owner's harness.command.result event from the feed and waits on settled({ inputId }).
  • Three tests in tests/input-lane.test.ts: a forwarded command that returns a value, one the owner refuses (unknown_command, invalid_input), and one that throws.
  • One bullet in docs/harness/durable-sessions.md, and one sentence in the unreleased ai-harness-core changeset. The harness is not released yet, so no new changeset.

Fixes for tests that already failed on feat/harness-core, so CI goes green:

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with pnpm run test:pr, or these tests do not apply to this pull request.
  • I fully understand the code in this pull request, including any code generated with AI assistance.
  • Docs: I updated docs/ for this change, or this change is not user-facing.
  • Changeset: I added a changeset (pnpm changeset), or this PR does not change a published package.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Root cause

Issue. Two durable hosts share one thread. Host A owns it and runs a turn. hostB.runCommand(...) resolves with undefined, also for an unknown command, bad input, or a command that throws.
Cause. executeCommand treated the offered admission like a duplicate. It called operation.finish('completed', undefined) at once, before the owner ran the command.
Fix. For offered, the host waits on settled({ inputId }). That rejects with InputRejectedError when the owner refuses the input. A failed or aborted settlement fails the operation. A completed settlement finishes it with the result from the owner's harness.command.result event, matched by the owner's operation id.

Possible alternatives

  • Receipt only. End the forwarded operation with a queued receipt and document that a forwarded command has no result. The caller still cannot tell a refused command from one that ran, so this PR did not take it.
  • Result in the settlement record. The owner writes the result into harness.input.settled. This changes a log record shape for one case, and the result event is already in the log.

Testing

Commands run.

  • Repro (agent-written, this session): npx vitest run tests/input-lane.test.ts -t "a command" in packages/ai-harness.
    • On clean feat/harness-core (79a946689) plus the tests only: 3 failed. expected undefined to be 'echo hi', and promise resolved "undefined" instead of rejecting (twice).
    • With the fix: 3 passed.
  • tests/input-lane.test.ts in full: 27 passed. The whole @tanstack/ai-harness suite passes (one test skipped, see above). ai-persistence conformance-skip-reporting: 87 passed.
  • pnpm test:types and pnpm test:oxlint in packages/ai-harness: pass (only old warnings). oxfmt --check: pass.
  • Skipped: full pnpm test:pr and E2E. CI runs them. The E2E suite has no two-host harness setup.

Manual test.

  1. Open one thread on two hosts that share a log store (see twoStores() in tests/input-lane.test.ts).
  2. Start a turn on host A, so A owns the thread.
  3. On host B, call runCommand({ name: 'unknown' }). Before this PR it resolved with undefined. Now it rejects with InputRejectedError and the reason unknown_command.
  4. On host B, run a command that returns a value. The operation resolves with that value.

How this PR makes testing easy. The three tests under a command in tests/input-lane.test.ts are the repro.

Linked issues

Closes #1796

Risk / rollback

Low. Only a command forwarded to another host changes. If the owner's result event is evicted from the feed before the input settles, the result is undefined, as before. Revert the PR to undo.

…error

A command that reaches a host that does not own the thread now waits for
the owner to settle it. It resolves with the owner's result, fails when the
command throws, and rejects with InputRejectedError when the owner refuses
it (unknown_command, invalid_input). Before, it completed with undefined.

Closes #1796
@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 7a0b833

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 5s View ↗

☁️ Nx Cloud last updated this comment at 2026-10-22 08:43:36 UTC

@pkg-pr-new

pkg-pr-new Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@1872

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@1872

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@1872

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@1872

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@1872

@tanstack/ai-byteplus

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-byteplus@1872

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@1872

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@1872

@tanstack/ai-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-cloudflare@1872

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@1872

@tanstack/ai-code-mode-snippets

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-snippets@1872

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@1872

@tanstack/ai-cohere

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-cohere@1872

@tanstack/ai-compaction

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-compaction@1872

@tanstack/ai-computer-local

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-computer-local@1872

@tanstack/ai-computer-playwright

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-computer-playwright@1872

@tanstack/ai-computer-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-computer-sandbox@1872

@tanstack/ai-computer-use

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-computer-use@1872

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@1872

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@1872

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@1872

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@1872

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@1872

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@1872

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@1872

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@1872

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@1872

@tanstack/ai-harness

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-harness@1872

@tanstack/ai-harness-cli

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-harness-cli@1872

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@1872

@tanstack/ai-isolate-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-daytona@1872

@tanstack/ai-isolate-e2b

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-e2b@1872

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@1872

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@1872

@tanstack/ai-isolate-quickjs-bun

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs-bun@1872

@tanstack/ai-llmgateway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-llmgateway@1872

@tanstack/ai-lovable

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-lovable@1872

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@1872

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@1872

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@1872

@tanstack/ai-octane

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-octane@1872

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@1872

@tanstack/ai-ollaya

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollaya@1872

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@1872

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@1872

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@1872

@tanstack/ai-perplexity

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-perplexity@1872

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@1872

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@1872

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@1872

@tanstack/ai-reactor

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-reactor@1872

@tanstack/ai-remix

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-remix@1872

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@1872

@tanstack/ai-sandbox-blaxel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-blaxel@1872

@tanstack/ai-sandbox-boxd

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-boxd@1872

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@1872

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@1872

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@1872

@tanstack/ai-sandbox-e2b

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-e2b@1872

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@1872

@tanstack/ai-sandbox-railway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-railway@1872

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@1872

@tanstack/ai-sandbox-upstash-box

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-upstash-box@1872

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@1872

@tanstack/ai-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-skills@1872

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@1872

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@1872

@tanstack/ai-typesafe

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-typesafe@1872

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@1872

@tanstack/ai-vercel-gateway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vercel-gateway@1872

@tanstack/ai-vertex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vertex@1872

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@1872

@tanstack/ai-worldlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-worldlabs@1872

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@1872

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@1872

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@1872

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@1872

@tanstack/svelte-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/svelte-ai-devtools@1872

commit: 4ffc8b1

A child resumes from its stored pending messages. The store keeps a nested
child in its own thread, so those messages had no card for it, and the
nested child ran again from the start. Keep the nested cards from the
client's copy on the stored message of their parent tool call.

Also update the harnessText test for the required threadId: one session per
thread.
@changeset-bot

changeset-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4ffc8b1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes changesets to release 8 packages
Name Type
@tanstack/ai-harness Major
@tanstack/ai-acp Patch
@tanstack/ai-code-mode Patch
@tanstack/ai-computer-use Patch
@tanstack/ai-harness-cli Patch
@tanstack/ai-mcp Patch
@tanstack/ai-sandbox Patch
@tanstack/ai-skills 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

…twin to the chat-only conformance

The stopgap in spawn.ts copied nested subagent cards from the client, which
trusts the client's messages for a nested child. #1878 fixes the nested
resume from the store. Until then, skip the subagent-waits test that needs it.

The chat-only conformance test from main had no twin. On this branch
memoryPersistence has a log store, so the log-across-instances cases need one.
@tombeckenham
tombeckenham merged commit c121f26 into feat/harness-core Oct 22, 2026
13 of 14 checks passed
@tombeckenham
tombeckenham deleted the 1796-ai-harness-a-forwarded-command-completes-with-no-result-also-when-the-owner-rejects-it branch October 22, 2026 08:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants