Repository navigation
Platform-level relay reply: every agent converses over the relay by default #254
Description
Problem
Deployed agents can receive relay DMs today (persona relay.inbox: ['@self'] → cloud inbox_selectors → candidates → deliver → tick wakes the box), but they cannot reply over the relay. A relay DM from a local/peer agent wakes the target, but the answer goes to the wrong place:
@agentworkforce/deliveryonly knows'slack' | 'telegram'targets (DeliveryClient.targets,resolveDeliveryTargets,onlyTargets,SlackRef/TelegramRefare all hardcoded to those two).- So an agent's relay Q&A branch (e.g.
hn-monitorhandleQaMessage(provider:'relay')) replies via the delivery package to Slack, never back to the relay sender.hn-monitor.parseRelayMessageeven drops the sender. - There is no
relaycastwriteback provider in@relayfile/adapter-core(the catalog is external integrations only), and no handler-side relay-send primitive.
Net: "local agent ↔ deployed agent over the relay" doesn't round-trip. Patching each persona (hn-monitor, then inbox-buddy, then every future agent) is the wrong layer. Every agent should be on the relay and able to converse by default, with zero per-persona code.
Proposed design (workforce)
1. packages/delivery — add relaycast as a first-class target.
- Widen the target union
'slack' | 'telegram'→'slack' | 'telegram' | 'relaycast'acrossDeliveryClient.targets,resolveDeliveryTargets(ctx),createDelivery(ctx, transports?, onlyTargets?), and addRelaycastRefalongsideSlackRef/TelegramRef. - The relaycast transport posts via the relaycast API using
RELAY_API_KEY+RELAY_WORKSPACE_ID(already injected into every box bypackages/corelauncher in cloud) to the inbound relay event'schannel/threadId(carried onRelaycastMessageEvent—@agent-relay/events). resolveDeliveryTargetsincludesrelaycastwhen the current run is handling a relay DM (origin-transport reply), so the existing "reply to origin transport" pattern in hn-monitor/inbox-buddy/joke-bot covers relay automatically — no persona changes.
2. packages/runtime — expose relay reply on ctx.
- Add
ctx.relay.reply(text)/ctx.relay.send(toAgent, text)sugar for agents that don't use the delivery package, backed by the same relaycast client. - Surface the inbound sender +
channel/threadon the relay event so replies address the right DM/thread (todayRelaycastMessageEventcarrieschannel/messageIdbut personas re-parse ad hoc and drop the sender).
3. (cross-repo) AgentWorkforce/cloud — relay-on by default.
- Default
inbox_selectorsto['@self']for every deployed agent intranslatePersonaRelayToInboxSelectors(packages/web/lib/proactive-runtime/inbox-selectors.ts), opt-out viarelay.enabled:false, so addressability is the default rather than opt-in. (Receiving side; complements the reply primitive above.)
Migration / cleanup
- Once delivery has the
relaycasttarget, drop the per-persona Slack-fallback hack inagentsrepohn-monitorrelay branch; inbox-buddy/joke-bot inherit relay reply for free.
Acceptance
A local (broker-spawned) agent DMs a deployed agent (e.g. hn-monitor) over the relay and gets the answer back in the relay (full round-trip), with zero persona-specific relay code. Verified against a real workspace (rw_* on the cloud-configured relaycast gateway).
Context
Surfaced while wiring local-agent ↔ cloud-agent chat for the AgentWorkforce/agents personas (inbox-buddy/joke-bot/spotify/hn-monitor dual-transport unification). Relates to cloud inbox delivery (#2212) and the optional-integrations work (#252).
Activity
khaliqgant commentedon Jun 25, 2026
pt.1 — delivery
relaycasttarget: #255 (merged-pending). Adds relaycast as a first-class delivery target so the existing origin-reply pattern covers agent-to-agent relay replies with no per-persona code.Gateway URL decision: default is now
https://cast.agentrelay.com(DEFAULT_RELAYCAST_URL, single source of truth inpackages/delivery/src/relaycast.ts), overridable per-env viaRELAYCAST_URL>RELAY_BASE_URL.Remaining:
ctx.relay.reply()+ surface inbound sender on the relay event (so callers populatetoergonomically)inbox_selectorsto['@self'](relay-on by default)@agentworkforce/delivery, bump in agents repo, drop hn-monitor's slack-only relay fallbackfix(delivery): address relaycast-target review feedback (#255)
khaliqgant commentedon Jun 25, 2026
Update — review pass + a newly-surfaced blocker.
relaycasttarget (feat(delivery): relaycast target — agents reply over the relay (#254 pt.1) #255): review feedback addressed —fetchWithTimeout, agent-token auth for/v1/dm(not the workspace key), unwrap{ok,data}for the message id, missing-id = failure, and relaycast excluded from non-blockingpublish(). 8/8 green.deliverRelaycastEventForDeployments(ws, "@self", …)fans a DM to every@selfdeployment, not the recipient. Defaulting@selfeverywhere would leak private DMs across agents.New required item (predecessor to relay-on-by-default):
@selfdeployment delivery to the DM recipient inservices/agent-gateway/src/durable-object.ts(deliverRelaycastEventForDeployments) — deliver only to the addressed recipient deployment; keep channel-selector fan-out. Un-drafts #2491.Also still open: runtime
ctx.relay.reply()+ surface inbound sender; converge box base-URL+key ontocast.agentrelay.com; publish delivery + consume in agents.khaliqgant commentedon Jun 25, 2026
Comprehensive status
Shipped / in review
relaycasttarget — feat(delivery): relaycast target — agents reply over the relay (#254 pt.1) #255 (review feedback addressed: fetchWithTimeout, agent-token auth,{ok,data}unwrap, missing-id=failure, excluded from non-blocking publish). 8/8 tests.ctx.relay— feat(runtime): ctx.relay — agent-to-agent messaging over the relay (#254 pt.2) #256 (ctx.relay.dm(to,text)/ctx.relay.post(channel,text); agent-token auth;RELAYCAST_URL>RELAY_BASE_URL>cast.agentrelay.com; inline to avoid the delivery→runtime circular dep). 5/5 tests.inbox_selectorsto@self; precedence fixed). Drafted pending B.B —
@selfrecipient-scoping (HARD blocker for un-drafting #2491). Root cause confirmed:services/agent-gateway/src/inbox-subscriber.tsRelaycastInboxMessageEventhasfrombut no recipient/to.durable-object.tshandleRelaycastSelfEvent→deliverRelaycastEventForDeployments(ws, "@self", …)fans to all@selfdeployment candidates (only excluding the sender) because, for cold deployments, it has no recipient identity to match against. Live agents get their own scoped@selfconnection; cold deployments are woken by the over-broad candidate fan-out. Fix needs the recipient identity on the relay event (relaycast engine + gateway) so cold-deployment@selfdelivery can match the addressed agent. Not a safe surgical edit — needs design + live testing. Until then, defaulting@selfeverywhere would leak DMs, so #2491 stays drafted.C — gateway URL →
cast.agentrelay.com@agentworkforce/delivery+@agentworkforce/runtimenow default tocast.agentrelay.com(single-const source of truth, env-overridable).infra/relaycast.tshostname(line ~21) tocast.agentrelay.com+ restore the proddomainbinding (line ~158, currently released to the legacy router), provision the Cloudflare custom domain/cert, and re-mint existing workspace keys against cast (today'srk_live_*validate onapi.relaycast.dev, are rejected by cast — the "Invalid API key" we saw).D — agents consumption (gated on publish)
After #255 + #256 merge and
@agentworkforce/{delivery,runtime}publish: bump them in the agents repo, drop hn-monitor's Slack-only relay fallback, and have its relay Q&A reply viactx.relay.dm(<sender>, …). Needs the sender on the event (see B) for a true reply-to-sender; until then it can reply to a known peer.Merge order
@agentworkforce/delivery@agentworkforce/runtime@selfrecipient-scoping) — unblocks #2491khaliqgant commentedon Jun 25, 2026
Blocker B now has a detailed design issue: AgentWorkforce/cloud#2494 (recipient-scope
@selfrelay DM delivery). It's the gate for un-drafting cloud#2491 and for true reply-to-sender viactx.relay.feat(delivery): add relaycast target
feat(runtime): ctx.relay — agent-to-agent messaging over the relay (#254
kjgbot commentedon Jun 25, 2026
Closed by workforce#255 (delivery relaycast target, merged) and workforce#256 (ctx.relay, merged).
khaliqgant commentedon Jun 26, 2026
Correction — canonical gateway + repo scoping.
The relaycast gateway's going-forward home is the
AgentWorkforce/relaycast-cloudrepo, and it servescast.agentrelay.com(prod) on the agentrelay.com zone (relaycast-cloud/infra/relaycast.ts+ README).gateway.relaycast.dev/api.relaycast.devare legacy hosts, fronted by relaycast-cloud's strangler router during cutover. So:@agentworkforce/{delivery,runtime}→cast.agentrelay.com) were correct.relaycast-cloud, NOT the standalonerelaycastrepo orcloud/packages/relaycast(legacy).@selfrecipient-scoping (cloud#2494) correctly stays incloud(agent infra).C re-scoped + done in code: the cast gateway itself already exists (relaycast-cloud). The remaining cloud-side change — point
resolveRelaycastUrl()(key minting + every agent box'sRELAY_BASE_URL) atcast.agentrelay.comso all proactive agents go on the canonical gateway — is cloud#2501 (draft). Its only deploy prerequisite is the key migration: existing workspace keys minted onapi.relaycast.devmust be re-minted against the cast backend.Updated merge order:
khaliqgant commentedon Jun 26, 2026
All pieces now have landed code. Status:
relaycasttargetctx.relay@selfrecipient-scoping (no-leak)@self)cast.agentrelay.comgatewayRemaining to actually round-trip in prod:
ctx.relay).relaycast-cloud) backend — the one ops/migration step. New workspaces are self-consistent on cast immediately.summary.actorsender field path against the live relay; the hn-monitor resolver degrades to Slack if it differs.The platform itself (send/reply + relay-on + no-leak + canonical gateway in code) is complete.
fix(hn-monitor): address #92 review feedback
feat(hn-monitor): reply over the relay to the sender (consume #254 pl…