Summary
#355 closes the concrete actions.invoke() retry defect from #354, but the TypeScript SDK uses the same automatic retry loop for every HTTP method. A source inventory at main + #355 head found other POST surfaces with no idempotency key and no stable client-supplied mutation id. A lost committed response can therefore duplicate work or rotate credentials more than once.
Confirmed high-risk surfaces
| SDK surface |
Engine behavior per attempt |
Ambiguous retry consequence |
relay.agents.spawn() / release() |
/v1/agents/spawn and /v1/agents/release each call actionEngine.invokeAction(), which mints a fresh invocation id |
The exact distinct-invocation duplicate class fixed for /actions/:name/invoke remains on these legacy entry points |
agent.files.upload() |
createUpload() generates a new file/storage key and presigned URL, then inserts a pending row |
Orphan/duplicate pending upload records and URLs |
relay.subscriptions.create() |
createSubscription() generates sub_<id> and inserts an active row |
Duplicate outbound delivery subscriptions |
relay.triggers.create() |
createTrigger() generates trg_<id> and inserts an enabled row |
Duplicate action firing |
relay.certify.submit() / monitor() |
creates a new certification id; submit immediately runs the external certification |
Duplicate stored runs and repeated external work |
relay.agents.events.emit() |
appends a new evt_<id> with the next sequence |
Duplicate session events |
| webhook/token create and token rotation methods |
mint new random ids/secrets or replace a credential |
Lost-response retry can create an inaccessible resource, return a conflict instead of the committed secret, or rotate more than once |
The client call sites are currently in packages/sdk-typescript/src/agent.ts and packages/sdk-typescript/src/relay.ts; the retry loop in src/client.ts retries network exceptions and 429/500/502/503/504 without method gating.
Likely naturally idempotent / lower-risk surfaces
Presence heartbeat/disconnect, membership join/leave, mute/unmute, reactions/read receipts with uniqueness, delivery ack/fail/defer, action completion by invocation id, and action registration by name appear set-like or keyed already. They still need contract tests before being declared safe; this list is triage, not proof.
Required follow-up
- Classify every SDK POST as keyed/idempotent, create/append, credential rotation, or intentionally ambiguous.
- Reuse the workspace/actor-scoped
Idempotency-Key contract for create/append operations, preserving one generated key across retries.
- Add stable route-level keys to both legacy agent spawn/release surfaces or route them through the now-idempotent action endpoint.
- For credential rotations, either replay the original secret for a bounded key or disable automatic retry and surface an explicit ambiguous outcome.
- Add one lost-response MUST-NOT-DUPLICATE regression per behavior class, plus same-key/different-payload conflicts.
Related: #354, #355, AgentWorkforce/relay#1604.
Summary
#355 closes the concrete
actions.invoke()retry defect from #354, but the TypeScript SDK uses the same automatic retry loop for every HTTP method. A source inventory at main + #355 head found other POST surfaces with no idempotency key and no stable client-supplied mutation id. A lost committed response can therefore duplicate work or rotate credentials more than once.Confirmed high-risk surfaces
relay.agents.spawn()/release()/v1/agents/spawnand/v1/agents/releaseeach callactionEngine.invokeAction(), which mints a fresh invocation id/actions/:name/invokeremains on these legacy entry pointsagent.files.upload()createUpload()generates a new file/storage key and presigned URL, then inserts a pending rowrelay.subscriptions.create()createSubscription()generatessub_<id>and inserts an active rowrelay.triggers.create()createTrigger()generatestrg_<id>and inserts an enabled rowrelay.certify.submit()/monitor()relay.agents.events.emit()evt_<id>with the next sequenceThe client call sites are currently in
packages/sdk-typescript/src/agent.tsandpackages/sdk-typescript/src/relay.ts; the retry loop insrc/client.tsretries network exceptions and 429/500/502/503/504 without method gating.Likely naturally idempotent / lower-risk surfaces
Presence heartbeat/disconnect, membership join/leave, mute/unmute, reactions/read receipts with uniqueness, delivery ack/fail/defer, action completion by invocation id, and action registration by name appear set-like or keyed already. They still need contract tests before being declared safe; this list is triage, not proof.
Required follow-up
Idempotency-Keycontract for create/append operations, preserving one generated key across retries.Related: #354, #355, AgentWorkforce/relay#1604.