You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* SDKs: node provider clients (TS, Python, Swift) + node.spawn/node.message frames
Add a node provider client to each relaycast SDK: connect to /v1/node/ws with a
node token, declare provider identity, register capabilities with per-capability
acceptance, run action.invoke handlers, heartbeat, reconnect with a fresh
instance id, and deregister. Handler context exposes sendMessage and spawnAgent.
Wire the ctx helpers to two node-WS request frames: node.spawn (capacity-direct,
routing dispatchCapacitySpawn) and node.message (post + observer fanout).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Test node.message attribution is scoped to the node workspace
Assert a node token cannot post as an agent from another workspace: the
`from` lookup is workspace-scoped, so a cross-workspace name is rejected
with agent_not_found and no message leaks into the other workspace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Scope node.spawn to the connection's own node
Drop target_node_id from the node.spawn frame: ctx.spawnAgent addresses the
provider's own node capacity executor, and the engine always dispatches to the
connection's node. A node credential cannot direct a spawn at another node —
cross-node placement is workspace-level with agent/workspace credentials.
Conformance asserts the spawn lands on the connection's own node and never
reaches a second node's capacity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: reconnect on handshake drop; drop unsupported thread_id
- TS and Python clients now treat a transport drop during the register
handshake as a reconnect (only a protocol rejection hard-fails), matching
Swift. Fixes a Python hang and a TS non-reconnect when the socket closes
before the node.register reply.
- Remove thread_id from node.message: the field was accepted by the wire
schema and all three SDKs but silently dropped by the engine (postMessage
posts top-level only). node.message is a top-level-post surface; thread
replies are not part of it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: validate acceptance, sync handlers, spawn input type
- All three clients hard-fail registration when the reply omits a well-formed
accepted_capabilities list (a missing/corrupt list means acceptance can't be
confirmed); a malformed acceptance entry counts as rejected (fail-safe). Swift
previously dropped malformed entries and could register on a corrupt reply.
- Python handlers may be sync or async (awaited only when awaitable), matching
the TypeScript client.
- Swift spawnAgent takes [String: JSONValue] so callers can't build a node.spawn
frame the engine's object-typed input schema rejects.
- Strengthen the reconnect test to complete re-registration after the drop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ctx.sendMessage posts through the canonical message route, not a parallel frame
Remove the node.message WS frame and its engine handler. Node-attributed posts
now go through the canonical POST /v1/channels/:name/messages route, which
accepts a node token (new `sender` auth requirement) with a required `from` —
an agent resolved strictly within the node token's workspace. Delivery routing,
observer events, webhooks, triggers, and idempotency come free because it is the
one posting path (spec §5.3: only the hosting connection differs).
ctx.sendMessage in all three SDKs becomes an HTTP call to that route with the
node token. node.spawn stays a WS frame (dispatchCapacitySpawn duplicates no
existing surface).
Conformance covers node-token posting: delivery to node-hosted recipients,
required `from`, unknown-agent and cross-workspace rejection, and `from`
rejected on an agent token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: settle registration on reconnect exhaustion; null-safe reply
- All three clients reject the registration waiter when reconnects are exhausted
before registering, so serve()/whenRegistered() no longer hang forever.
- TS onRegistered tolerates a null/undefined reply data (treats it as an invalid
reply hard-fail) instead of throwing a TypeError that misroutes to reconnect.
- Guard the Python reconnect tests against a race reading the re-register frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: registration-state guard, HTTP base/encoding hardening
- Python: reconnect exhaustion no longer overwrites a completed registration, so
wait_registered() keeps returning success after a later drop.
- ctx.sendMessage normalizes a ws(s):// base to http(s):// (all three SDKs) and
encodes the channel as a single path segment (Swift no longer leaves `/`
unescaped). Swift treats a non-HTTP or non-envelope 2xx response as a failure
instead of returning null.
- Poll for the node-token delivery fanout in conformance instead of a fixed
sleep, matching the file's other delivery assertions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: verify full acceptance, drain on stop, stop rejects register
- Registration hard-fails unless every requested capability is acknowledged in
the reply (an empty/partial accepted_capabilities can no longer pass) — all
three SDKs.
- stop() drains in-flight invoke handlers (bounded) before closing the socket so
their action.result frames aren't dropped, and rejects a still-pending
registration waiter so whenRegistered() can't hang when stopped pre-register.
- Swift clamps the reconnect max delay against the sanitized base delay (no
negative backoff) and its HTTP tests clear the mock URL handler via defer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: drain gate, snapshot validation, drain self-safety
- stop() stops accepting new action.invoke frames once draining, so a late
invoke can't start a handler whose result would be dropped after close (all
three SDKs); the engine reschedules the undispatched invocation.
- Registration acceptance is validated against the capability snapshot sent in
the register frame, not the live map, so a capability added mid-handshake
can't trigger a false missing-acknowledgement failure (all three SDKs).
- Python stop() excludes the caller's own task from the drain, so a handler that
calls stop() no longer deadlocks (and gets cancelled, skipping deregister).
- TS clears the drain timeout when tasks finish early; Swift stops heartbeats
before the drain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
0 commit comments