fix(a2a): serve agent-card discovery at the standard well-known path - #319
Conversation
Agent-card discovery is where A2A federation starts, and it does not work today on any deployment whose workspace is not named after the first label of its hostname. Verified against production, where the hosted workspace is named rw_7ccfea89: GET /.well-known/agent-card.json -> 404 workspace_not_found GET /.well-known/agent-card.json?workspace=rw_7ccfea89 -> 200 GET /rw_7ccfea89/.well-known/agent-card.json -> 404 workspace_not_found GET /.well-known/agent-card.json (Bearer token) -> 200 Two defects, one root cause. extractWorkspaceHint resolved query -> host label -> path param, and the unauthenticated card handler had no other source. 1. The documented /:workspace/.well-known/agent-card.json route was dead on any authority with three or more labels, because host inference always answered first and the path parameter was never read. The Relay identifier profile requires multi-label, non-loopback authorities, so every conformant deployment sits exactly where that route cannot work. 2. A single-tenant self-host could not answer the bare standard URL at all, which is the one an A2A counterparty tries first. The path parameter now beats host inference, and the card handler falls back to the sole workspace when a deployment has exactly one. The fallback is skipped whenever a query or path selector was supplied, so a misspelled selector fails loudly rather than quietly resolving to another tenant, and the candidate query is bounded to two rows so multi-tenant deployments fail closed without scanning the table. POST /a2a/rpc is unaffected: it authenticates and takes the workspace from the token. That is what made this narrow and misleading — discovery 404s while everything authenticated works, and the error text blames agent registration. Six tests, including the one that makes the change safe to ship: a valid hosted workspace subdomain stays authoritative, since this reorders live routing. Split out of the Phase 2 federation work so it can ship on its own; it affects any A2A counterparty, not only that engagement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughWorkspace agent-card discovery now prioritizes explicit path selectors and supports bare well-known URLs for single-workspace deployments. API documentation, route classification, integration tests, and changelogs describe the updated behavior. ChangesWorkspace agent-card discovery
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant handleWorkspaceAgentCard
participant extractWorkspaceHint
participant WorkspaceLookup
Client->>handleWorkspaceAgentCard: Request agent card
handleWorkspaceAgentCard->>extractWorkspaceHint: Resolve path or host workspace
handleWorkspaceAgentCard->>WorkspaceLookup: Query up to two workspaces
WorkspaceLookup-->>handleWorkspaceAgentCard: Return workspace match or ambiguity
handleWorkspaceAgentCard-->>Client: Return agent card or workspace_not_found
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
| if ( | ||
| !workspace | ||
| && !c.req.query('workspace') | ||
| && !c.req.param('workspace') | ||
| ) { | ||
| const candidates = await db.select().from(workspaces).limit(2); | ||
| if (candidates.length === 1) { | ||
| workspace = candidates[0]!; | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Discovery endpoint behaviour changed without matching API documentation updates
The public agent-card discovery endpoint changed how it picks a workspace (new sole-workspace fallback added at packages/engine/src/routes/a2a.ts:244-253 and reordered selection at packages/engine/src/routes/a2a.ts:65-80) without the required matching updates to README.md and openapi.yaml, so the published API description no longer matches the server.
Impact: Integrators reading the docs cannot tell how discovery resolves a workspace, including the /:workspace/ path form and the ?workspace= selector.
Repository documentation rule and what is missing
AGENTS.md ("Docs Hygiene") mandates: "Update README.md and openapi.yaml together when API behavior changes." This PR changes observable behaviour of GET /.well-known/agent-card.json (a bare request on a single-workspace deployment now returns 200 instead of 404) and makes the /:workspace/.well-known/agent-card.json route functional for the first time on multi-label authorities. README.md:652 lists only the bare route, and openapi.yaml:4796-4810 documents no workspace query parameter, no path-scoped variant, and no 404 workspace_not_found response. Neither file is touched by this PR.
Prompt for agents
AGENTS.md requires README.md and openapi.yaml to be updated together whenever API behaviour changes. This PR changes the behaviour of GET /.well-known/agent-card.json (single-workspace deployments now answer the bare path; an explicit /:workspace/ path selector now takes precedence over host inference) but does not touch the docs. Update openapi.yaml's /.well-known/agent-card.json entry to document the optional ?workspace= query parameter, the /:workspace/.well-known/agent-card.json path variant, and the 404 workspace_not_found response, and add the path-scoped route to the A2A route list in README.md.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if ( | ||
| !workspace | ||
| && !c.req.query('workspace') | ||
| && !c.req.param('workspace') | ||
| ) { | ||
| const candidates = await db.select().from(workspaces).limit(2); | ||
| if (candidates.length === 1) { | ||
| workspace = candidates[0]!; | ||
| } | ||
| } |
There was a problem hiding this comment.
🟨 Unauthenticated agent-card discovery can leak a workspace's agent roster when host selector is wrong
The unauthenticated discovery handler now falls back to the only workspace in the database whenever no query/path selector is present (packages/engine/src/routes/a2a.ts:244-253). This fallback also triggers when a host-label selector was supplied but did not resolve, so any request to any hostname on a deployment holding exactly one workspace returns that workspace's card, which enumerates every agent name in the workspace (getWorkspaceAgentCard). On the hosted gateway this makes an internal workspace's agent roster reachable without credentials at a time when the table holds a single workspace.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Line 23: Shorten the release-note bullet in CHANGELOG.md at lines 23-23 to one
concise, impact-first statement covering standard agent-card discovery and
explicit workspace path selection. Also shorten packages/engine/CHANGELOG.md at
lines 14-14 to one concise bullet describing the workspace-selection contract;
remove failure mechanisms, private helper names, query details, and
implementation backstory from both entries.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2102354e-b708-48c5-8e18-649b38ac03fe
📒 Files selected for processing (4)
CHANGELOG.mdpackages/engine/CHANGELOG.mdpackages/engine/src/engine/__tests__/a2a.test.tspackages/engine/src/routes/a2a.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37be358d22
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| && !c.req.query('workspace') | ||
| && !c.req.param('workspace') | ||
| ) { | ||
| const candidates = await db.select().from(workspaces).limit(2); |
There was a problem hiding this comment.
Update README and OpenAPI with the discovery contract
This changes the public agent-card resolution contract by adding sole-workspace fallback and making the path selector authoritative, but README.md still lists only the root route and openapi.yaml documents neither selector nor fallback/error semantics. Consumers therefore cannot discover the supported /:workspace/ form or determine when the bare URL returns 404; update both sources alongside this behavior change.
AGENTS.md reference: AGENTS.md:L35-L35
Useful? React with 👍 / 👎.
| !workspace | ||
| && !c.req.query('workspace') | ||
| && !c.req.param('workspace') |
There was a problem hiding this comment.
Treat an empty workspace query as explicit
When a deployment has exactly one workspace and a caller sends ?workspace=, the query value is an empty string, so both extraction and this truthiness guard treat the selector as absent and return the sole workspace's card. This silently replaces a malformed explicit selector despite the new fail-loud contract; test parameter presence separately from whether its value is nonempty so this request returns workspace_not_found.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
1 issue found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/engine/src/routes/a2a.ts">
<violation number="1" location="packages/engine/src/routes/a2a.ts:246">
P3: An empty `?workspace=` selector is treated as absent and returns the sole workspace card rather than failing closed. Check query-key presence in the fallback guard so supplied-but-empty selectors produce `workspace_not_found`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // closed without scanning the workspace table. | ||
| if ( | ||
| !workspace | ||
| && !c.req.query('workspace') |
There was a problem hiding this comment.
P3: An empty ?workspace= selector is treated as absent and returns the sole workspace card rather than failing closed. Check query-key presence in the fallback guard so supplied-but-empty selectors produce workspace_not_found.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/engine/src/routes/a2a.ts, line 246:
<comment>An empty `?workspace=` selector is treated as absent and returns the sole workspace card rather than failing closed. Check query-key presence in the fallback guard so supplied-but-empty selectors produce `workspace_not_found`.</comment>
<file context>
@@ -228,6 +235,23 @@ async function handleWorkspaceAgentCard(c: Context<AppEnv>) {
+ // closed without scanning the workspace table.
+ if (
+ !workspace
+ && !c.req.query('workspace')
+ && !c.req.param('workspace')
+ ) {
</file context>
| && !c.req.query('workspace') | |
| && !new URL(c.req.url).searchParams.has('workspace') |
…k boundary Devin (docs): AGENTS.md requires README.md and openapi.yaml to move with API behaviour. Documents the resolution order, the ?workspace= selector, the path-scoped route, and the 404 workspace_not_found response for both forms. Devin (security): the sole-workspace fallback also fires when a host label was inferred but did not resolve. Keeping that is deliberate and now explained in code: an explicit query/path selector is caller intent and must 404 on a typo, whereas host-label inference is a hosted convention that always yields a candidate on any authority with three or more labels. Treating it as explicit would mean the fallback never fires on the deployments it exists for. The row cap is the actual boundary — with two or more workspaces it declines rather than guessing, so no tenant boundary can be crossed, and the card is unauthenticated by design. Added a test asserting the single-tenant behaviour directly, so it is stated intent rather than an accident; the existing multi-tenant test already covers the same request 404ing. CodeRabbit: changelog entries trimmed to one impact-first bullet each, per the repo guideline. Engine suite 559/559 across 52 files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Review addressed in Devin — docs parity. Correct, and it is a repo rule I missed. Devin — sole-workspace fallback firing after an unresolved host label. Keeping this, deliberately, and the reasoning is now in the code rather than implied. The guard distinguishes two things that look alike. An explicit The row cap is the real boundary: with two or more workspaces the fallback declines rather than guessing, so there is no tenant boundary to cross. On a deployment holding exactly one workspace there is only one tenant, and the card is unauthenticated by design — that is what A2A discovery is. Rather than leave that as an argument, it is now asserted both ways:
CodeRabbit — changelog concision. Fair; both entries were carrying implementation backstory. Trimmed to one impact-first bullet each. |
…metadata Devin 🔴 / cubic P1 — the SDK's automatic key transform rewrote the inner field names of message `data` and `metadata`. For a Ratify envelope that is not cosmetic: a RevocationList whose `revoked_certs` arrives as `revokedCerts` cannot be reconstructed byte-for-byte, so its signature no longer verifies and a cross-deployment revocation is rejected for the wrong reason. Both keys join VERBATIM_VALUE_KEYS alongside `headers`, `input` and `input_schema`, which are already exempt on exactly this reasoning: their keys are caller data, not wire protocol. BEHAVIOUR CHANGE, called out in the SDK changelog rather than buried: callers who relied on the SDK snake_casing their metadata keys on the wire now see the keys exactly as written, including for agent registration metadata. The setup.test.ts assertion that expected `favorite_color` encoded the corruption rather than the contract — a caller who deliberately wrote `favoriteColor` got a different key back — so that expectation is corrected rather than preserved. Reviewers should weigh this: it is the one part of this PR that changes behaviour for existing consumers. Codex P1 — inbound A2A `message/send` called sendDm directly, bypassing the idempotency the DM route already applies to every send. `sendToExternalAgent` retries on 5xx, and the counter, webhook, workspace event and delivery routing all run after the durable write, so a transient failure there delivered the counterparty's proof or task twice. Inbound now runs through runIdempotent, keyed on the caller's message_id (falling back to the JSON-RPC id) and scoped to the registered caller so two counterparties cannot collide. Replays skip the side effects and return the original result. cubic P1 — Swift's CoreMessagePayload silently dropped the newly preserved metadata during decoding, so Swift consumers could not see Ratify envelopes at all. Added as optional [String: JSONValue], mirroring CoreMessagePayloadSchema. Both new tests verified non-vacuous: reverting only the casing change fails the two round-trip assertions, and disabling only the idempotency key delivers the retried message twice. Also drops the agent-card discovery changes from this branch — they ship separately in #319, so the two PRs are now disjoint. engine 53 files / 556 tests, sdk-typescript 419 tests, full turbo build green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught what my local run did not: adding a path to openapi.yaml requires
classifying it in packages/types' sdk-openapi-sync test as either SDK-covered or
intentionally non-SDK. The path-scoped agent-card route is the latter, for the
same reason as the bare form already listed beside it — unauthenticated A2A
discovery is not an agent-SDK surface.
I had run the engine and sdk-typescript suites and inferred the rest. Full
• Packages in scope: @relaycast/a2a, @relaycast/engine, @relaycast/mcp, @relaycast/observer-dashboard, @relaycast/openclaw, @relaycast/react, @relaycast/sdk, @relaycast/types, relaycast
• Running test in 9 packages
• Remote caching disabled, using shared worktree cache
@relaycast/a2a:build: cache hit, replaying logs a458c4ed01107517
@relaycast/a2a:build:
@relaycast/a2a:build: > @relaycast/a2a@7.0.0 build
@relaycast/a2a:build: > rm -rf dist tsconfig.tsbuildinfo && tsc
@relaycast/a2a:build:
@relaycast/types:build: cache hit, replaying logs 2c2c15f9a185bd51
@relaycast/types:build:
@relaycast/types:build: > @relaycast/types@7.0.0 build
@relaycast/types:build: > rm -rf dist tsconfig.tsbuildinfo && tsc
@relaycast/types:build:
@relaycast/a2a:test: cache hit, replaying logs 14539744c6a0e79a
@relaycast/types:test: cache hit, replaying logs bfcaf87044a67093
@relaycast/types:test:
@relaycast/types:test: > @relaycast/types@7.0.0 test
@relaycast/types:test: > vitest run
@relaycast/types:test:
@relaycast/types:test:
@relaycast/types:test: �[1m�[30m�[46m RUN �[49m�[39m�[22m �[36mv4.1.10 �[39m�[90m/private/tmp/disc-split/packages/types�[39m
@relaycast/types:test:
@relaycast/types:test: �[32m✓�[39m src/__tests__/sdk-openapi-sync.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 2�[2mms�[22m�[39m
@relaycast/types:test: �[32m✓�[39m src/__tests__/event-id.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 2�[2mms�[22m�[39m
@relaycast/types:test: �[32m✓�[39m src/__tests__/telemetry.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 11�[2mms�[22m�[39m
@relaycast/types:test: �[32m✓�[39m src/__tests__/types.test.ts �[2m(�[22m�[2m36 tests�[22m�[2m)�[22m�[32m 6�[2mms�[22m�[39m
@relaycast/types:test: �[32m✓�[39m src/__tests__/fleet-wire-fixtures.test.ts �[2m(�[22m�[2m25 tests�[22m�[2m)�[22m�[32m 19�[2mms�[22m�[39m
@relaycast/types:test: �[32m✓�[39m src/__tests__/events.test.ts �[2m(�[22m�[2m91 tests�[22m�[2m)�[22m�[32m 22�[2mms�[22m�[39m
@relaycast/types:test:
@relaycast/types:test: �[2m Test Files �[22m �[1m�[32m6 passed�[39m�[22m�[90m (6)�[39m
@relaycast/types:test: �[2m Tests �[22m �[1m�[32m163 passed�[39m�[22m�[90m (163)�[39m
@relaycast/types:test: �[2m Start at �[22m 13:59:43
@relaycast/types:test: �[2m Duration �[22m 425ms�[2m (transform 715ms, setup 0ms, import 1.15s, tests 64ms, environment 0ms)�[22m
@relaycast/types:test:
@relaycast/a2a:test:
@relaycast/a2a:test: > @relaycast/a2a@7.0.0 test
@relaycast/a2a:test: > vitest run --passWithNoTests
@relaycast/a2a:test:
@relaycast/a2a:test:
@relaycast/a2a:test: �[1m�[30m�[46m RUN �[49m�[39m�[22m �[36mv4.1.10 �[39m�[90m/private/tmp/disc-split/packages/a2a�[39m
@relaycast/a2a:test:
@relaycast/a2a:test: �[32m✓�[39m src/__tests__/schemas.test.ts �[2m(�[22m�[2m62 tests�[22m�[2m)�[22m�[32m 14�[2mms�[22m�[39m
@relaycast/a2a:test:
@relaycast/a2a:test: �[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m
@relaycast/a2a:test: �[2m Tests �[22m �[1m�[32m62 passed�[39m�[22m�[90m (62)�[39m
@relaycast/a2a:test: �[2m Start at �[22m 13:59:42
@relaycast/a2a:test: �[2m Duration �[22m 218ms�[2m (transform 52ms, setup 0ms, import 102ms, tests 14ms, environment 0ms)�[22m
@relaycast/a2a:test:
@relaycast/sdk:build: cache hit, replaying logs 339abf92be4d4365
@relaycast/sdk:build:
@relaycast/sdk:build: > @relaycast/sdk@7.0.0 build
@relaycast/sdk:build: > tsc
@relaycast/sdk:build:
@relaycast/openclaw:build: cache hit, replaying logs 8e480d460ef51321
@relaycast/openclaw:build:
@relaycast/openclaw:build: > @relaycast/openclaw@7.0.0 build
@relaycast/openclaw:build: > tsc
@relaycast/openclaw:build:
@relaycast/openclaw:test: cache hit, replaying logs 026f2a4985d6ddaa
@relaycast/mcp:build: cache hit, replaying logs df96a76760111a0b
@relaycast/sdk:test: cache hit, replaying logs 30ef527af4c37095
@relaycast/openclaw:test:
@relaycast/openclaw:test: > @relaycast/openclaw@7.0.0 test
@relaycast/openclaw:test: > vitest run --root ../.. packages/openclaw/src/__tests__/bridge.test.ts packages/openclaw/src/__tests__/config.test.ts packages/openclaw/src/__tests__/setup.test.ts
@relaycast/openclaw:test:
@relaycast/openclaw:test:
@relaycast/openclaw:test: RUN v3.2.7 /private/tmp/disc-split
@relaycast/openclaw:test:
@relaycast/openclaw:test: ✓ packages/openclaw/src/__tests__/setup.test.ts (3 tests) 6ms
@relaycast/openclaw:test: ✓ packages/openclaw/src/__tests__/config.test.ts (5 tests) 3ms
@relaycast/openclaw:test: ✓ packages/openclaw/src/__tests__/bridge.test.ts (11 tests) 8ms
@relaycast/openclaw:test:
@relaycast/openclaw:test: Test Files 3 passed (3)
@relaycast/openclaw:test: Tests 19 passed (19)
@relaycast/openclaw:test: Start at 13:59:49
@relaycast/openclaw:test: Duration 630ms (transform 221ms, setup 0ms, collect 334ms, tests 17ms, environment 0ms, prepare 317ms)
@relaycast/openclaw:test:
@relaycast/mcp:build:
@relaycast/mcp:build: > @relaycast/mcp@7.0.0 build
@relaycast/mcp:build: > tsc
@relaycast/mcp:build:
@relaycast/sdk:test:
@relaycast/sdk:test: > @relaycast/sdk@7.0.0 test
@relaycast/sdk:test: > vitest run
@relaycast/sdk:test:
@relaycast/sdk:test:
@relaycast/sdk:test: �[1m�[30m�[46m RUN �[49m�[39m�[22m �[36mv4.1.10 �[39m�[90m/private/tmp/disc-split/packages/sdk-typescript�[39m
@relaycast/engine:build: cache hit, replaying logs 54ae5d8711716441
@relaycast/sdk:test:
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/programmability.test.ts �[2m(�[22m�[2m29 tests�[22m�[2m)�[22m�[32m 30�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/invite-consumption.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 16�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/relay.test.ts �[2m(�[22m�[2m69 tests�[22m�[2m)�[22m�[33m 880�[2mms�[22m�[39m
@relaycast/sdk:test: �[33m�[2m✓�[22m�[39m requires apiKey �[33m 784�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/ws.test.ts �[2m(�[22m�[2m33 tests�[22m�[2m)�[22m�[32m 169�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/agent-ws.test.ts �[2m(�[22m�[2m37 tests�[22m�[2m)�[22m�[32m 150�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/node-provider.test.ts �[2m(�[22m�[2m18 tests�[22m�[2m)�[22m�[32m 300�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/errors.test.ts �[2m(�[22m�[2m43 tests�[22m�[2m)�[22m�[32m 34�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/agent-messaging.test.ts �[2m(�[22m�[2m33 tests�[22m�[2m)�[22m�[32m 10�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/strict-identity.test.ts �[2m(�[22m�[2m14 tests�[22m�[2m)�[22m�[33m 388�[2mms�[22m�[39m
@relaycast/sdk:test: �[33m�[2m✓�[22m�[39m registers agent with exact name �[33m 380�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/agent-features.test.ts �[2m(�[22m�[2m34 tests�[22m�[2m)�[22m�[32m 46�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/harness.test.ts �[2m(�[22m�[2m25 tests�[22m�[2m)�[22m�[33m 428�[2mms�[22m�[39m
@relaycast/sdk:test: �[33m�[2m✓�[22m�[39m is available from the package root �[33m 378�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/type-safety.test.ts �[2m(�[22m�[2m3 tests�[22m�[2m)�[22m�[32m 1�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/communicate.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 90�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/identity.test.ts �[2m(�[22m�[2m18 tests�[22m�[2m)�[22m�[33m 407�[2mms�[22m�[39m
@relaycast/sdk:test: �[33m�[2m✓�[22m�[39m forwards identity onto an agent socket too, not just the observer socket �[33m 402�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/deliveries.test.ts �[2m(�[22m�[2m8 tests�[22m�[2m)�[22m�[32m 9�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/client-retry.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 25�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/casing.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 51�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/workspace.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 286�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/event-id.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 4�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/index.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/relay-connection.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[33m 329�[2mms�[22m�[39m
@relaycast/sdk:test: �[32m✓�[39m src/__tests__/setup.test.ts �[2m(�[22m�[2m20 tests�[22m�[2m)�[22m�[33m 3437�[2mms�[22m�[39m
@relaycast/sdk:test: �[33m�[2m✓�[22m�[39m createWorkspace() accepts 200 success responses �[33m 666�[2mms�[22m�[39m
@relaycast/sdk:test: �[33m�[2m✓�[22m�[39m createWorkspace() surfaces API errors with status and body �[33m 2717�[2mms�[22m�[39m
@relaycast/sdk:test:
@relaycast/sdk:test: �[2m Test Files �[22m �[1m�[32m22 passed�[39m�[22m�[90m (22)�[39m
@relaycast/sdk:test: �[2m Tests �[22m �[1m�[32m417 passed�[39m�[22m�[90m (417)�[39m
@relaycast/sdk:test: �[2m Start at �[22m 13:59:45
@relaycast/sdk:test: �[2m Duration �[22m 4.05s�[2m (transform 4.00s, setup 0ms, import 7.31s, tests 7.09s, environment 2ms)�[22m
@relaycast/sdk:test:
@relaycast/engine:build:
@relaycast/engine:build: > @relaycast/engine@7.0.0 build
@relaycast/engine:build: > tsc && node -e "require('fs').cpSync('src/db/migrations','dist/db/migrations',{recursive:true})"
@relaycast/engine:build:
@relaycast/react:build: cache hit, replaying logs 9ed587b55f4950e5
@relaycast/react:build:
@relaycast/react:build: > @relaycast/react@7.0.0 build
@relaycast/react:build: > tsc
@relaycast/react:build:
@relaycast/mcp:test: cache hit, replaying logs 025d73b448b58e29
relaycast:build: cache hit, replaying logs dc46d3a60d94fbbe
@relaycast/engine:test: cache hit, replaying logs 2a11bf1a828fb39d
@relaycast/mcp:test:
@relaycast/mcp:test: > @relaycast/mcp@7.0.0 test
@relaycast/mcp:test: > vitest run
@relaycast/mcp:test:
@relaycast/mcp:test:
@relaycast/mcp:test: �[1m�[30m�[46m RUN �[49m�[39m�[22m �[36mv4.1.10 �[39m�[90m/private/tmp/disc-split/packages/mcp�[39m
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mappends unread channels to tool response
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=1 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mappends unread DMs to tool response
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=1 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mappends mentions to tool response
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=1 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mdoes not append when inbox is empty
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mfilters out self-sent DMs from piggyback
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=2 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mfilters out self-mentions from piggyback
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=2 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mfilters self-sent DMs with case and @ prefix differences
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=2 dms=3 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2msuppresses piggyback entirely when all messages are self-sent
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=1 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mappends recent reactions to tool response
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=1
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mfilters out self-reactions from piggyback
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=2
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2msuppresses piggyback when only self-reactions exist
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=1
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mhandles inbox error gracefully
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch failed — Network error
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mfilters self-authored items using the routed workspace identity
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=2 dms=2 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/piggyback.test.ts�[2m > �[22m�[2mpiggyback unread messages�[2m > �[22m�[2mfalls back to routing.as for self-filtering when no identity resolver is provided
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=2 dms=2 reactions=2
@relaycast/mcp:test:
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/piggyback.test.ts �[2m(�[22m�[2m17 tests�[22m�[2m)�[22m�[32m 115�[2mms�[22m�[39m
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/messaging-tools.test.ts �[2m(�[22m�[2m13 tests�[22m�[2m)�[22m�[33m 1044�[2mms�[22m�[39m
@relaycast/mcp:test: �[33m�[2m✓�[22m�[39m post_message calls send() �[33m 446�[2mms�[22m�[39m
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/registration-tools.test.ts �[2m(�[22m�[2m19 tests�[22m�[2m)�[22m�[33m 1135�[2mms�[22m�[39m
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/ws-bridge.test.ts �[2m(�[22m�[2m19 tests�[22m�[2m)�[22m�[33m 302�[2mms�[22m�[39m
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/server-multi-workspace.test.ts�[2m > �[22m�[2mmulti-workspace server setup�[2m > �[22m�[2mmessage.post with workspace_id routes to correct workspace
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/server-multi-workspace.test.ts�[2m > �[22m�[2mmulti-workspace server setup�[2m > �[22m�[2mmessage.post without workspace routing uses the active workspace identity
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/observer-dashboard:build: cache hit, replaying logs 9a67e4a8948ac9e7
relaycast:build:
relaycast:build: > relaycast@7.0.0 build
relaycast:build: > tsc
relaycast:build:
@relaycast/engine:test:
@relaycast/engine:test: > @relaycast/engine@7.0.0 test
@relaycast/engine:test: > vitest run
@relaycast/engine:test:
@relaycast/engine:test:
@relaycast/engine:test: �[1m�[30m�[46m RUN �[49m�[39m�[22m �[36mv4.1.10 �[39m�[90m/private/tmp/disc-split/packages/engine�[39m
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for malformed bodies handled by the shared parser
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'6eb95cda-16d9-427b-8906-74c273832ff3'�[39m,
@relaycast/engine:test: route: �[32m'/v1/triggers'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m3�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_agent_token'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'b0966a840da20881'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2mlists the queued delivery for a recipient with the message payload
@relaycast/engine:test: �[22m�[39m[delivery.route] provider not delivery-ready; delivery deferred {
@relaycast/engine:test: workspace_id: �[32m'212540436826226688'�[39m,
@relaycast/engine:test: node_id: �[32m'node_direct_212540436897529856'�[39m,
@relaycast/engine:test: provider_name: �[32m'default'�[39m,
@relaycast/engine:test: agent_id: �[32m'212540436897529856'�[39m,
@relaycast/engine:test: delivery_id: �[32m'del_212540437287600128_212540436897529856'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for malformed bodies that still flow through route catches
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'd1751920-7d22-4035-a4ed-fceb0bb03a32'�[39m,
@relaycast/engine:test: route: �[32m'/v1/channels'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m11�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_agent_token'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'42e7cf5a834e9c3f'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for malformed channel update request bodies
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'71685fbd-43d9-4a97-bda6-2720448dfea5'�[39m,
@relaycast/engine:test: route: �[32m'/v1/channels/malformed-update-channel'�[39m,
@relaycast/engine:test: method: �[32m'PATCH'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m1�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_workspace_key'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'47cca9f02db1a83a'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2mdoes not expose deliveries to non-recipients
@relaycast/engine:test: �[22m�[39m[delivery.route] provider not delivery-ready; delivery deferred {
@relaycast/engine:test: workspace_id: �[32m'212540437639921664'�[39m,
@relaycast/engine:test: node_id: �[32m'node_direct_212540437753167872'�[39m,
@relaycast/engine:test: provider_name: �[32m'default'�[39m,
@relaycast/engine:test: agent_id: �[32m'212540437753167872'�[39m,
@relaycast/engine:test: delivery_id: �[32m'del_212540437887385600_212540437753167872'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2macks a delivery idempotently and removes it from the default queue
@relaycast/engine:test: �[22m�[39m[delivery.route] provider not delivery-ready; delivery deferred {
@relaycast/engine:test: workspace_id: �[32m'212540438994681856'�[39m,
@relaycast/engine:test: node_id: �[32m'node_direct_212540439024041984'�[39m,
@relaycast/engine:test: provider_name: �[32m'default'�[39m,
@relaycast/engine:test: agent_id: �[32m'212540439024041984'�[39m,
@relaycast/engine:test: delivery_id: �[32m'del_212540439074373632_212540439024041984'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mkeeps custom validation messages when routes use the shared parser
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'74079be9-bb74-4b28-a65c-f30779bbbd21'�[39m,
@relaycast/engine:test: route: �[32m'/v1/subscriptions'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m14�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_workspace_key'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'644c9f60d4f63f9c'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_request'�[39m,
@relaycast/engine:test: error_reason: �[32m'events array is required'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for another route migrated to the shared parser
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'6f0d22b5-ef35-4ca1-b519-62f75c1158b8'�[39m,
@relaycast/engine:test: route: �[32m'/v1/subscriptions'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m1�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_workspace_key'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'db19f86aace6309e'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/nodeProviders.test.ts�[2m > �[22m�[2mnode providers�[2m > �[22m�[2mattaches, replaces on reconnect, and rejects a duplicate live instance
@relaycast/engine:test: �[22m�[39m[node.control] rejected message {
@relaycast/engine:test: type: �[32m'node.register'�[39m,
@relaycast/engine:test: code: �[32m'provider_instance_conflict'�[39m,
@relaycast/engine:test: workspaceId: �[32m'212540439496306688'�[39m,
@relaycast/engine:test: nodeId: �[32m'node_a'�[39m,
@relaycast/engine:test: provider: �[32m'py'�[39m,
@relaycast/engine:test: message: �[32m'Provider "py" is already connected on this node'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2mrecords a failed delivery with error text and retryability
@relaycast/engine:test: �[22m�[39m[delivery.route] provider not delivery-ready; delivery deferred {
@relaycast/engine:test: workspace_id: �[32m'212540439741267968'�[39m,
@relaycast/engine:test: node_id: �[32m'node_direct_212540439762239488'�[39m,
@relaycast/engine:test: provider_name: �[32m'default'�[39m,
@relaycast/engine:test: agent_id: �[32m'212540439762239488'�[39m,
@relaycast/engine:test: delivery_id: �[32m'del_212540440089395200_212540439762239488'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for malformed directory request bodies
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'60bcd16a-8cc9-4460-b228-3ee6c9ce4251'�[39m,
@relaycast/engine:test: route: �[32m'/v1/directory/agents'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m3�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_workspace_key'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'336cdfeefc94800e'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2mdefers a delivery with available_at and keeps it in the queue
@relaycast/engine:test: �[22m�[39m[delivery.route] provider not delivery-ready; delivery deferred {
@relaycast/engine:test: workspace_id: �[32m'212540440940838912'�[39m,
@relaycast/engine:test: node_id: �[32m'node_direct_212540440953421824'�[39m,
@relaycast/engine:test: provider_name: �[32m'default'�[39m,
@relaycast/engine:test: agent_id: �[32m'212540440953421824'�[39m,
@relaycast/engine:test: delivery_id: �[32m'del_212540440986976256_212540440953421824'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/nodeProviders.test.ts�[2m > �[22m�[2mnode providers�[2m > �[22m�[2mstill rejects a direct SDK instance through the attach-liveness boundary
@relaycast/engine:test: �[22m�[39m[node.control] rejected message {
@relaycast/engine:test: type: �[32m'node.register'�[39m,
@relaycast/engine:test: code: �[32m'provider_instance_conflict'�[39m,
@relaycast/engine:test: workspaceId: �[32m'212540441044004864'�[39m,
@relaycast/engine:test: nodeId: �[32m'node_a'�[39m,
@relaycast/engine:test: provider: �[32m'default'�[39m,
@relaycast/engine:test: message: �[32m'Provider "default" is already connected on this node'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for malformed file upload request bodies
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'642e8b43-c3cd-4bc2-8aac-5b849d6cbb87'�[39m,
@relaycast/engine:test: route: �[32m'/v1/files/upload'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m2�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_agent_token'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'd303d27ace9e6719'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for malformed direct message request bodies
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'c11e13f4-68b3-4c8b-9396-902f492d9a15'�[39m,
@relaycast/engine:test: route: �[32m'/v1/dm'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m8�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_agent_token'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'0a87a802e21a919e'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/nodeProviders.test.ts�[2m > �[22m�[2mnode providers�[2m > �[22m�[2mrejects a second provider registering an action another provider already owns on the node
@relaycast/engine:test: �[22m�[39m[node.control] rejected message {
@relaycast/engine:test: type: �[32m'node.register'�[39m,
@relaycast/engine:test: code: �[32m'action_name_conflict'�[39m,
@relaycast/engine:test: workspaceId: �[32m'212540441723482112'�[39m,
@relaycast/engine:test: nodeId: �[32m'node_a'�[39m,
@relaycast/engine:test: provider: �[32m'rb'�[39m,
@relaycast/engine:test: message: �[32m'Action "run-etl" is already registered by provider "py" on this node'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/node.test.ts�[2m > �[22m�[2mnode adapter conformance�[2m > �[22m�[2mworkspace stream�[2m > �[22m�[2mfans out published events to workspace-stream sockets
@relaycast/engine:test: �[22m�[39m[telemetry] captureException TypeError: The database connection is not open
@relaycast/engine:test: at Database.prepare (/private/tmp/disc-split/node_modules/�[4mbetter-sqlite3�[24m/lib/methods/wrappers.js:5:21)
@relaycast/engine:test: at BetterSQLiteSession.prepareQuery (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/better-sqlite3/session.js:23:30)
@relaycast/engine:test: at BetterSQLiteSession.prepareOneTimeQuery (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/sqlite-core/session.js:141:17)
@relaycast/engine:test: at QueryPromise._prepare (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/sqlite-core/query-builders/delete.js:86:81)
@relaycast/engine:test: at QueryPromise.execute (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/sqlite-core/query-builders/delete.js:114:17)
@relaycast/engine:test: at QueryPromise.then (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/query-promise.js:21:17) { source: �[32m'node.event_queue'�[39m, op: �[32m'retention_prune'�[39m }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2mdoes not resurrect an acked (terminal) delivery via defer or fail
@relaycast/engine:test: �[22m�[39m[delivery.route] provider not delivery-ready; delivery deferred {
@relaycast/engine:test: workspace_id: �[32m'212540441444155392'�[39m,
@relaycast/engine:test: node_id: �[32m'node_direct_212540441653870592'�[39m,
@relaycast/engine:test: provider_name: �[32m'default'�[39m,
@relaycast/engine:test: agent_id: �[32m'212540441653870592'�[39m,
@relaycast/engine:test: delivery_id: �[32m'del_212540441981026304_212540441653870592'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/node.test.ts�[2m > �[22m�[2mnode adapter conformance�[2m > �[22m�[2mrate limiter�[2m > �[22m�[2mallows up to the limit then blocks within the window
@relaycast/engine:test: �[22m�[39m[telemetry] captureException TypeError: The database connection is not open
@relaycast/engine:test: at Database.prepare (/private/tmp/disc-split/node_modules/�[4mbetter-sqlite3�[24m/lib/methods/wrappers.js:5:21)
@relaycast/engine:test: at BetterSQLiteSession.prepareQuery (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/better-sqlite3/session.js:23:30)
@relaycast/engine:test: at BetterSQLiteSession.prepareOneTimeQuery (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/sqlite-core/session.js:141:17)
@relaycast/engine:test: at QueryPromise._prepare (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/sqlite-core/query-builders/delete.js:86:81)
@relaycast/engine:test: at QueryPromise.execute (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/sqlite-core/query-builders/delete.js:114:17)
@relaycast/engine:test: at QueryPromise.then (file:///private/tmp/disc-split/node_modules/�[4mdrizzle-orm�[24m/query-promise.js:21:17) { source: �[32m'node.event_queue'�[39m, op: �[32m'retention_prune'�[39m }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for malformed channel message request bodies
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'7a31c072-7f3e-4e92-a90a-3e6bd7ca3cdc'�[39m,
@relaycast/engine:test: route: �[32m'/v1/channels/general/messages'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m1�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_agent_token'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'8fa1bfed523cb9f3'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2mallows recovering a failed delivery via ack (retryable failures are not terminal)
@relaycast/engine:test: �[22m�[39m[delivery.route] provider not delivery-ready; delivery deferred {
@relaycast/engine:test: workspace_id: �[32m'212540442958299136'�[39m,
@relaycast/engine:test: node_id: �[32m'node_direct_212540442983464960'�[39m,
@relaycast/engine:test: provider_name: �[32m'default'�[39m,
@relaycast/engine:test: agent_id: �[32m'212540442983464960'�[39m,
@relaycast/engine:test: delivery_id: �[32m'del_212540443029602304_212540442983464960'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for malformed group DM request bodies
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'221170b7-81e4-478a-acac-41f94af5e510'�[39m,
@relaycast/engine:test: route: �[32m'/v1/dm/group'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m0�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_agent_token'�[39m,
@relaycast/react:test: cache hit, replaying logs 8b42683d0f84ff07
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/server-multi-workspace.test.ts�[2m > �[22m�[2mmulti-workspace server setup�[2m > �[22m�[2mmessage.post with workspace_id and as uses the registered workspace identity
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/server-multi-workspace.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[33m 579�[2mms�[22m�[39m
@relaycast/mcp:test: �[33m�[2m✓�[22m�[39m message.post with workspace_id routes to correct workspace �[33m 346�[2mms�[22m�[39m
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/channel-tools.test.ts �[2m(�[22m�[2m14 tests�[22m�[2m)�[22m�[32m 99�[2mms�[22m�[39m
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2muses agent token for authenticated requests after register
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mclears a routed invalid token without clearing the active workspace token
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2msends mcp origin headers on relay API requests
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mworkspace.join registers in the new workspace and uses the restored agent token
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mworkspace.switch restores the previous workspace agent token from workspace_ref
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mworkspace.switch accepts the canonical workspace_name from the API
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mchannel.create → POST /v1/channels
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mchannel.list → GET /v1/channels
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mchannel.join → POST /v1/channels/:name/join
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mchannel.leave → POST /v1/channels/:name/leave
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/server.test.ts�[2m > �[22m�[2mcreateRelayMcpServer�[2m > �[22m�[2msupports legacy flat tool calls without advertising legacy names
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mchannel.invite → POST /channels/:name/invite {agent_name}
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mchannel.set_topic → PATCH /v1/channels/:name/topic
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mchannel.archive → DELETE /v1/channels/:name
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/server.test.ts�[2m > �[22m�[2mcreateRelayMcpServer�[2m > �[22m�[2mregister tool works and enables other tools
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/server.test.ts�[2m > �[22m�[2mcreateRelayMcpServer�[2m > �[22m�[2mauto-bootstraps when agentToken and agentName are provided
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/multi-agent-piggyback.test.ts�[2m > �[22m�[2mmulti-agent piggyback routing�[2m > �[22m�[2mfetches piggyback inbox from the routed workspace_id context
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=1 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.post → POST /v1/channels/:name/messages
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/multi-agent-piggyback.test.ts�[2m > �[22m�[2mmulti-agent piggyback routing�[2m > �[22m�[2mfetches piggyback inbox from the routed workspace_alias context
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=1 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/programmability-tools.test.ts �[2m(�[22m�[2m14 tests�[22m�[2m)�[22m�[33m 498�[2mms�[22m�[39m
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/multi-agent-piggyback.test.ts�[2m > �[22m�[2mmulti-agent piggyback routing�[2m > �[22m�[2mpasses routed workspace and requested as identity to piggyback inbox fetches
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=1 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/feature-tools.test.ts �[2m(�[22m�[2m13 tests�[22m�[2m)�[22m�[33m 538�[2mms�[22m�[39m
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.list → GET /v1/channels/:name/messages
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.reply → POST /v1/messages/:id/replies
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/multi-agent-piggyback.test.ts�[2m > �[22m�[2mmulti-agent piggyback routing�[2m > �[22m�[2mstill piggybacks routed inbox state when the active session token is missing
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=1 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/multi-agent-piggyback.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[33m 540�[2mms�[22m�[39m
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.get_thread → GET /v1/messages/:id/replies
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.dm.send → POST /v1/dm
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.dm.list → GET /v1/dm/conversations
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/multi-agent-integration.test.ts�[2m > �[22m�[2mmulti-agent integration routing�[2m > �[22m�[2muses the routed workspace for both message dispatch and piggyback inbox fetch
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=1 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/multi-agent-integration.test.ts�[2m > �[22m�[2mmulti-agent integration routing�[2m > �[22m�[2muses the requested routed agent identity for both message dispatch and piggyback inbox fetches
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=1 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/multi-agent-integration.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[33m 599�[2mms�[22m�[39m
@relaycast/mcp:test: �[33m�[2m✓�[22m�[39m uses the routed workspace for both message dispatch and piggyback inbox fetch �[33m 463�[2mms�[22m�[39m
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.dm.send_group → POST /v1/dm/group then POST /v1/dm/:id/messages
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/server.test.ts �[2m(�[22m�[2m10 tests�[22m�[2m)�[22m�[33m 2754�[2mms�[22m�[39m
@relaycast/mcp:test: �[33m�[2m✓�[22m�[39m lists all 45 tools �[33m 369�[2mms�[22m�[39m
@relaycast/mcp:test: �[33m�[2m✓�[22m�[39m supports legacy flat tool calls without advertising legacy names �[33m 1083�[2mms�[22m�[39m
@relaycast/mcp:test: �[33m�[2m✓�[22m�[39m supports keyless startup and bootstrap via set_workspace_key �[33m 310�[2mms�[22m�[39m
@relaycast/mcp:test: �[33m�[2m✓�[22m�[39m retries WS bridge initialization after switching away from a token that failed WS init �[33m 499�[2mms�[22m�[39m
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.reaction.add → POST /v1/messages/:id/reactions
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.reaction.remove → DELETE /v1/messages/:id/reactions/:emoji
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.search → GET /v1/search?q=...
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[32m✓�[39m src/__tests__/multi-agent-bridge.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[33m 1152�[2mms�[22m�[39m
@relaycast/mcp:test: �[33m�[2m✓�[22m�[39m stops and recreates the bridge when the active agent token changes across workspaces �[33m 734�[2mms�[22m�[39m
@relaycast/mcp:test: �[33m�[2m✓�[22m�[39m does not recreate the bridge when the agent token changes inside the same workspace �[33m 417�[2mms�[22m�[39m
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.inbox.mark_read → POST /v1/messages/:id/read
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2mmessage.file.upload → POST /v1/files/upload
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/mcp:test:
@relaycast/mcp:test: �[90mstderr�[2m | src/__tests__/integration.test.ts�[2m > �[22m�[2mMCP → SDK → HTTP integration�[2m > �[22m�[2magent.list → GET /v1/agents
@relaycast/mcp:test: �[22m�[39m[piggyback] inbox fetch ok — channels=0 mentions=0 dms=0 reactions=0
@relaycast/react:test:
@relaycast/react:test: > @relaycast/react@7.0.0 test
@relaycast/react:test: > vitest run
@relaycast/react:test:
@relaycast/react:test:
@relaycast/react:test: RUN v3.2.7 /private/tmp/disc-split/packages/react
@relaycast/react:test:
@relaycast/react:test: ✓ src/__tests__/reducer.test.ts (25 tests) 9ms
@relaycast/react:test: ✓ src/__tests__/messageAdapters.test.ts (3 tests) 3ms
@relaycast/react:test: ✓ src/__tests__/provider.test.tsx (1 test) 302ms
@relaycast/react:test: ✓ src/__tests__/fetchRaceGuards.test.tsx (3 tests) 508ms
@relaycast/react:test: ✓ React data fetch race guards > ignores stale useMessages responses after the channel changes 467ms
@relaycast/react:test: ✓ src/__tests__/useMessages.test.tsx (2 tests) 803ms
@relaycast/react:test: ✓ useMessages > normalizes initial history to chronological order 470ms
@relaycast/react:test: ✓ useMessages > keeps chronological order when fetching older pages 333ms
@relaycast/react:test: ✓ src/__tests__/messageMarkdown.test.tsx (5 tests) 643ms
@relaycast/react:test: ✓ MessageMarkdown > renders highlighted fenced code and supports copy button 348ms
@relaycast/react:test:
@relaycast/react:test: Test Files 6 passed (6)
@relaycast/react:test: Tests 39 passed (39)
@relaycast/react:test: Start at 13:59:50
@relaycast/react:test: Duration 8.13s (transform 2.52s, setup 0ms, collect 12.78s, tests 2.27s, environment 16.56s, prepare 1.01s)
@relaycast/react:test:
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'f7ea9e10e208e5cb'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2mincludes queued deliveries in the agent detail pending_deliveries
@relaycast/engine:test: �[22m�[39m[delivery.route] provider not delivery-ready; delivery deferred {
@relaycast/engine:test: workspace_id: �[32m'212540443755216896'�[39m,
@relaycast/engine:test: node_id: �[32m'node_direct_212540443771994112'�[39m,
@relaycast/engine:test: provider_name: �[32m'default'�[39m,
@relaycast/engine:test: agent_id: �[32m'212540443771994112'�[39m,
@relaycast/engine:test: delivery_id: �[32m'del_212540443922989056_212540443771994112'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2mreturns invalid_json for malformed thread reply request bodies
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'f4e5d153-2c99-4616-92e5-0942b73f6a25'�[39m,
@relaycast/engine:test: route: �[32m'/v1/messages/212540443986472960/replies'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m1�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_agent_token'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'a14faa5565262f75'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_json'�[39m,
@relaycast/engine:test: error_reason: �[32m'Malformed JSON in request body'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/nodeProviders.test.ts�[2m > �[22m�[2mnode providers�[2m > �[22m�[2mshadows native spawn capacity with a provider action and never silently bypasses it
@relaycast/engine:test: �[22m�[39m[request] Request failed {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'45f52096-b278-42b1-a54c-b886a239805f'�[39m,
@relaycast/engine:test: route: �[32m'/v1/actions/spawn/invoke'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m503�[39m,
@relaycast/engine:test: status_class: �[32m'5xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m3�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_agent_token'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'c331498e09769645'�[39m,
@relaycast/engine:test: error_code: �[32m'handler_unavailable'�[39m,
@relaycast/engine:test: error_reason: �[32m'Provider "policy" is offline for action "spawn:claude"'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2muses the shared invalid_request envelope for query validation
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'ae5419f7-a845-42f0-9e09-9e1223fbe1ff'�[39m,
@relaycast/engine:test: route: �[32m'/v1/console/messages'�[39m,
@relaycast/engine:test: method: �[32m'GET'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m1�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_workspace_key'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'bcdbb04a6bd18fdc'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_request'�[39m,
@relaycast/engine:test: error_reason: �[32m'Invalid console message query'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2muses schema-backed validation messages for search query parameters
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'cbefcc68-887e-430e-a88d-f2b5199ab9bc'�[39m,
@relaycast/engine:test: route: �[32m'/v1/search'�[39m,
@relaycast/engine:test: method: �[32m'GET'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m5�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_workspace_key'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'3850494aec8ee444'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_request'�[39m,
@relaycast/engine:test: error_reason: �[32m'q (search query) is required'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/httpResponse.test.ts�[2m > �[22m�[2mroute response helpers�[2m > �[22m�[2muses schema-backed validation messages for search query parameters
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'b76e4dc8-f9d0-4f29-898d-0ebf8ec37ef1'�[39m,
@relaycast/engine:test: route: �[32m'/v1/search'�[39m,
@relaycast/engine:test: method: �[32m'GET'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m1�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_workspace_key'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'3850494aec8ee444'�[39m,
@relaycast/engine:test: error_code: �[32m'invalid_request'�[39m,
@relaycast/engine:test: error_reason: �[32m'Invalid search query'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2mrejects a malformed JSON fail body with 400 (no state change)
@relaycast/engine:test: �[22m�[39m[delivery.route] provider not delivery-ready; delivery deferred {
@relaycast/engine:test: workspace_id: �[32m'212540444455665664'�[39m,
@relaycast/engine:test: node_id: �[32m'node_direct_212540444501803008'�[39m,
@relaycast/engine:test: provider_name: �[32m'default'�[39m,
@relaycast/engine:test: agent_id: �[32m'212540444501803008'�[39m,
@relaycast/engine:test: delivery_id: �[32m'del_212540444640215040_212540444501803008'�[39m
@relaycast/engine:test: }
@relaycast/engine:test:
@relaycast/engine:test: �[90mstderr�[2m | src/__tests__/conformance/delivery.test.ts�[2m > �[22m�[2mdurable delivery api�[2m > �[22m�[2mrejects a malformed JSON fail body with 400 (no state change)
@relaycast/engine:test: �[22m�[39m[request] Request client error {
@relaycast/engine:test: app_version: �[32m'0.1.0'�[39m,
@relaycast/engine:test: sdk_version: �[32m'unknown'�[39m,
@relaycast/engine:test: environment: �[32m'test'�[39m,
@relaycast/engine:test: source: �[32m'request'�[39m,
@relaycast/engine:test: request_id: �[32m'003efbbe-df0d-4578-8143-8af08ec256f6'�[39m,
@relaycast/engine:test: route: �[32m'/v1/deliveries/del_212540444640215040_212540444501803008/fail'�[39m,
@relaycast/engine:test: method: �[32m'POST'�[39m,
@relaycast/engine:test: origin_actor: �[32m'unknown'�[39m,
@relaycast/engine:test: status_code: �[33m400�[39m,
@relaycast/engine:test: status_class: �[32m'4xx'�[39m,
@relaycast/engine:test: duration_ms: �[33m12�[39m,
@relaycast/engine:test: route_group: �[32m'api_v1'�[39m,
@relaycast/engine:test: auth_scheme: �[32m'bearer_agent_token'�[39m,
@relaycast/engine:test: content_type: �[32m'application/json'�[39m,
@relaycast/engine:test: actor_fingerprint: �[32m'bcb193858df7d143'�[39m,
@r…
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@openapi.yaml`:
- Line 4820: Update the workspace parameter description in the OpenAPI
specification to avoid saying it is required for every multi-tenant deployment;
state that it is required only when the workspace cannot otherwise be resolved,
while preserving support for valid hosted workspace subdomains.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a652e509-6b31-494f-b3e7-295f68a6f0c8
📒 Files selected for processing (7)
CHANGELOG.mdREADME.mdopenapi.yamlpackages/engine/CHANGELOG.mdpackages/engine/src/engine/__tests__/a2a.test.tspackages/engine/src/routes/a2a.tspackages/types/src/__tests__/sdk-openapi-sync.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/engine/CHANGELOG.md
- CHANGELOG.md
- packages/engine/src/routes/a2a.ts
- packages/engine/src/engine/tests/a2a.test.ts
CodeRabbit, correct. The parameter description said workspace was required on any deployment holding more than one, but host-label inference resolves on its own — the integration test hits northwind.cast.example/.well-known/agent-card.json and gets 200 with no query parameter. The endpoint description had the same ambiguity: 'unless a selector identifies it' read as excluding the host label, which is one of the four mechanisms listed a line above. Both now say a valid workspace subdomain is sufficient on its own, while keeping the part that matters: an explicit query or path selector that does not resolve still 404s rather than falling back to another workspace. turbo test 18/18. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
CodeRabbit — Fixed in |
…metadata Devin 🔴 / cubic P1 — the SDK's automatic key transform rewrote the inner field names of message `data` and `metadata`. For a Ratify envelope that is not cosmetic: a RevocationList whose `revoked_certs` arrives as `revokedCerts` cannot be reconstructed byte-for-byte, so its signature no longer verifies and a cross-deployment revocation is rejected for the wrong reason. Both keys join VERBATIM_VALUE_KEYS alongside `headers`, `input` and `input_schema`, which are already exempt on exactly this reasoning: their keys are caller data, not wire protocol. BEHAVIOUR CHANGE, called out in the SDK changelog rather than buried: callers who relied on the SDK snake_casing their metadata keys on the wire now see the keys exactly as written, including for agent registration metadata. The setup.test.ts assertion that expected `favorite_color` encoded the corruption rather than the contract — a caller who deliberately wrote `favoriteColor` got a different key back — so that expectation is corrected rather than preserved. Reviewers should weigh this: it is the one part of this PR that changes behaviour for existing consumers. Codex P1 — inbound A2A `message/send` called sendDm directly, bypassing the idempotency the DM route already applies to every send. `sendToExternalAgent` retries on 5xx, and the counter, webhook, workspace event and delivery routing all run after the durable write, so a transient failure there delivered the counterparty's proof or task twice. Inbound now runs through runIdempotent, keyed on the caller's message_id (falling back to the JSON-RPC id) and scoped to the registered caller so two counterparties cannot collide. Replays skip the side effects and return the original result. cubic P1 — Swift's CoreMessagePayload silently dropped the newly preserved metadata during decoding, so Swift consumers could not see Ratify envelopes at all. Added as optional [String: JSONValue], mirroring CoreMessagePayloadSchema. Both new tests verified non-vacuous: reverting only the casing change fails the two round-trip assertions, and disabling only the idempotency key delivers the retried message twice. Also drops the agent-card discovery changes from this branch — they ship separately in #319, so the two PRs are now disjoint. engine 53 files / 556 tests, sdk-typescript 419 tests, full turbo build green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…metadata Devin 🔴 / cubic P1 — the SDK's automatic key transform rewrote the inner field names of message `data` and `metadata`. For a Ratify envelope that is not cosmetic: a RevocationList whose `revoked_certs` arrives as `revokedCerts` cannot be reconstructed byte-for-byte, so its signature no longer verifies and a cross-deployment revocation is rejected for the wrong reason. Both keys join VERBATIM_VALUE_KEYS alongside `headers`, `input` and `input_schema`, which are already exempt on exactly this reasoning: their keys are caller data, not wire protocol. BEHAVIOUR CHANGE, called out in the SDK changelog rather than buried: callers who relied on the SDK snake_casing their metadata keys on the wire now see the keys exactly as written, including for agent registration metadata. The setup.test.ts assertion that expected `favorite_color` encoded the corruption rather than the contract — a caller who deliberately wrote `favoriteColor` got a different key back — so that expectation is corrected rather than preserved. Reviewers should weigh this: it is the one part of this PR that changes behaviour for existing consumers. Codex P1 — inbound A2A `message/send` called sendDm directly, bypassing the idempotency the DM route already applies to every send. `sendToExternalAgent` retries on 5xx, and the counter, webhook, workspace event and delivery routing all run after the durable write, so a transient failure there delivered the counterparty's proof or task twice. Inbound now runs through runIdempotent, keyed on the caller's message_id (falling back to the JSON-RPC id) and scoped to the registered caller so two counterparties cannot collide. Replays skip the side effects and return the original result. cubic P1 — Swift's CoreMessagePayload silently dropped the newly preserved metadata during decoding, so Swift consumers could not see Ratify envelopes at all. Added as optional [String: JSONValue], mirroring CoreMessagePayloadSchema. Both new tests verified non-vacuous: reverting only the casing change fails the two round-trip assertions, and disabling only the idempotency key delivers the retried message twice. Also drops the agent-card discovery changes from this branch — they ship separately in #319, so the two PRs are now disjoint. engine 53 files / 556 tests, sdk-typescript 419 tests, full turbo build green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(a2a): federate Ratify metadata * address review: verbatim signed payloads, inbound idempotency, Swift metadata Devin 🔴 / cubic P1 — the SDK's automatic key transform rewrote the inner field names of message `data` and `metadata`. For a Ratify envelope that is not cosmetic: a RevocationList whose `revoked_certs` arrives as `revokedCerts` cannot be reconstructed byte-for-byte, so its signature no longer verifies and a cross-deployment revocation is rejected for the wrong reason. Both keys join VERBATIM_VALUE_KEYS alongside `headers`, `input` and `input_schema`, which are already exempt on exactly this reasoning: their keys are caller data, not wire protocol. BEHAVIOUR CHANGE, called out in the SDK changelog rather than buried: callers who relied on the SDK snake_casing their metadata keys on the wire now see the keys exactly as written, including for agent registration metadata. The setup.test.ts assertion that expected `favorite_color` encoded the corruption rather than the contract — a caller who deliberately wrote `favoriteColor` got a different key back — so that expectation is corrected rather than preserved. Reviewers should weigh this: it is the one part of this PR that changes behaviour for existing consumers. Codex P1 — inbound A2A `message/send` called sendDm directly, bypassing the idempotency the DM route already applies to every send. `sendToExternalAgent` retries on 5xx, and the counter, webhook, workspace event and delivery routing all run after the durable write, so a transient failure there delivered the counterparty's proof or task twice. Inbound now runs through runIdempotent, keyed on the caller's message_id (falling back to the JSON-RPC id) and scoped to the registered caller so two counterparties cannot collide. Replays skip the side effects and return the original result. cubic P1 — Swift's CoreMessagePayload silently dropped the newly preserved metadata during decoding, so Swift consumers could not see Ratify envelopes at all. Added as optional [String: JSONValue], mirroring CoreMessagePayloadSchema. Both new tests verified non-vacuous: reverting only the casing change fails the two round-trip assertions, and disabling only the idempotency key delivers the retried message twice. Also drops the agent-card discovery changes from this branch — they ship separately in #319, so the two PRs are now disjoint. engine 53 files / 556 tests, sdk-typescript 419 tests, full turbo build green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * changelog: state the measured scope of the casing change The previous entry led with 'Breaking', which is technically true and measurably alarmist. Checked against production before claiming a blast radius: 1025 agents, 723 with metadata, 8 multi-word keys in use (node_id, registered_at, cloud_workspace_id, cloud_agent_id, invocation_id, identity_key, room_membership_id, room_identity_id) — all stored snake_case. Single-word keys are identical in both casings and cannot be affected. Grepped relay, relaycast, cloud and the VS Code extension for the camelCase forms these would be read as today (metadata.nodeId, metadata.cloudWorkspaceId, …): zero hits. The one consumer that reads agent metadata at all, cloud/packages/web/lib/room/relaycast-access.ts, checks metadata.cloud_workspace_id in snake_case and reaches the API through globalThis.fetch rather than this SDK, so it reads the wire format and is unaffected either way. Entry now states what actually changes and for whom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * raise the pending SDK release to Major for the casing change cubic P2, accepted — with a note, because there is precedent pointing the other way and it deserves stating rather than silently overriding. 6.2.0 shipped the identical class of change (input_schema/output_schema, input/ output and headers stopped being key-rewritten) as a Minor, framed as a Fix, on the reasoning that rewriting user-authored keys was corruption rather than contract. By that precedent this entry is Minor too: data/metadata are the same kind of value and were simply missed from the same set. Taking Major anyway, because the two differ in one material way. input/output and headers are action payloads whose reader is essentially the code that produced them, so a rename is self-consistent. Agent and message metadata is written by one system and read by another — fleet and cloud write node_id, registered_at, cloud_workspace_id; other code reads them — and a silent rename across a system boundary is exactly where this bites. A behavioural change on a cross-system field-name contract should not ride a minor bump. Root and sdk-typescript headings raised; the engine and types headings stay Minor, since neither changes behaviour for existing callers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * address review: the remaining P2/P3 items Mailbox config (two comments, same defect): inbound A2A delivery passed only skipA2aIntercept, so sendDm fell back to its fixed one-hour / 1000-message defaults and registered-peer deliveries were the one path on the deployment exempt from its own configured TTL and depth cap. Now resolves and passes the mailbox config as /v1/dm does. Workspace/observer DM history dropped metadata: getDmMessagesForWorkspace never selected or projected messages.metadata, so a federated delivery's Ratify envelope was visible to the recipient and invisible in history — the same message reading differently depending on which door you came through. Projected through publicMessageMetadata, which strips the internal __relaycast_* keys. Idempotency fingerprint size: `data` was embedded whole. A proof bundle runs to 128 KiB, and the fingerprint is serialized into the stored record, kept for the TTL, and string-compared on every replay — roughly 256 KiB per DM in KV and a full-payload comparison each time. Now digested with sha256Hex, which answers the only question the fingerprint asks in constant size. Inbound acknowledgement latency: fanout, delivery routing and rejection notification were awaited, so the counterparty's "message accepted" waited on our recipient's delivery, including a slow HTTP-push receiver. Moved to runInBackground, matching /v1/dm. The durable write is what the response attests to. message/stream: accepted and then served as a one-shot, returning a task already terminal with no stream channel. A client calling message/stream expects a working task plus somewhere to subscribe; a completed one-shot is a wrong answer dressed as a right one and would silently truncate a conversation the caller believes is open. Now refused with -32601 and a message naming message/send, so the caller falls back immediately. NOTE: the agent card still advertises message/stream — that is a wider decision than this PR and is flagged rather than changed here. Base64 validation: issuer_pub_key and signature are documented as base64 but only checked non-empty, so a value that could never decode passed validation at the edge and failed later inside the verifier as an opaque error. Validated as standard base64. Existing test fixtures used placeholders ('pub-ed', 'sig-ed') that are not base64; replaced with real values and added cases for the rule. Proof-bundle size guard: the whole string was encoded before deciding it was oversized, so an attacker could force a multi-megabyte encode and a second same-size allocation before rejection — on unauthenticated inbound federation traffic. String length is checked first; UTF-8 cannot shrink, so length over the cap already proves byte length over it. Revocation test: asserts the received wire deep-equals the sent one, not just that its signature verifies. The engine is not the enforcement point — a Ratify verifier is — so the property the engine owns is that a signed document crosses unmodified. A key rename now fails with a readable diff rather than an opaque signature failure. turbo test 18/18 tasks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: accept unpadded base64 in Ratify hybrid key and signature fields cubic P1, and a regression I introduced while hardening validation. Requiring a multiple-of-four length rejected unpadded base64 — legal and commonly emitted — and the value it would have rejected is a signed revocation list. Over-strict validation there fails the kill switch closed over a formatting preference, which is worse than the loose check it replaced. Padding is now optional: a padded value must still be a multiple of four, and an unpadded one is rejected only at length ≡ 1 (mod 4), which no base64 quantum can produce. Tests cover 'QUJDRA' and 'QUJDRA==' (same bytes, both accepted) alongside the malformed cases. turbo test 18/18. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Split out of the Phase 2 federation work (#318) so it can ship on its own — it affects any A2A counterparty, not just that engagement.
Current production behaviour
Agent-card discovery is where A2A federation starts, and it does not work today on any deployment whose workspace is not named after the first label of its hostname. Verified against
cast.agentrelay.com, whose workspace is actually namedrw_7ccfea89:Two defects, one root cause
extractWorkspaceHintresolved query → host label → path param, and the unauthenticated card handler had no other source./:workspace/.well-known/agent-card.jsonroute was dead on any authority with three or more labels — host inference always answered first, so the path parameter was never read. The Relay identifier profile requires multi-label, non-loopback authorities, so every conformant deployment sits exactly where that escape hatch cannot work.POST /a2a/rpcis unaffected; it authenticates and takes the workspace from the token. That is what made this narrow and misleading: discovery 404s while everything authenticated succeeds, and the error text blames agent registration.The fix
Regression risk, and the test that covers it
This reorders live routing. The test that makes it safe to ship is "keeps a valid hosted workspace subdomain authoritative" — hosted multitenancy behaviour is unchanged.
Six tests total; the two subtle ones assert that the fallback never papers over a caller error.
Verification
packages/enginefull suite: 558 passed (558), 52 files — no regressionsa2a.test.ts: 33/33🤖 Generated with Claude Code