Managed getHistory (app-api /api/bots/history) doesn't include the in-flight
turn — unlike native adapters whose getHistory rebuilds the live thread — so
runAgent({context}) alone runs the agent with zero messages (→ provider 400).
Pass the current message (contentParts ?? text) as `prompt`, the sanctioned
mechanism for input not in the adapter's reconstructed history. Verified live:
the managed Slack bot now returns a real answer over the Phoenix loop.
The realtime primitives (startManagedBots, connectPhoenixHostedBotChannel,
PhoenixRealtimeTransport) existed but nothing composed them into a launcher, so
the managed path defaulted to HTTP and Phoenix was never actually used.
- startManagedBotsOnChannel(bots, { channel, scope, runtimeInstanceId }) — wraps
an already-connected channel in a PhoenixRealtimeTransport (delivery source +
render sink) and starts the bots via startManagedBots. Split out so the
behavior is unit-testable against a fake channel.
- startManagedBotsOverPhoenix(bots, config) — thin glue: connect the gateway
bot-IO channel, delegate, disconnect on stop().
- phoenixEgress: fail-loud EgressSink (Phoenix routes all egress through the
render sink).
- examples/slack/app/managed.ts — a REAL consumer of the launcher: the same
Slack bot as index.ts (agent/tools/context/commands/handlers identical) run in
managed mode over Phoenix instead of the native slack() adapter. No native
index.ts changes.
Tests drive a real createBot through the full managed path over a fake channel:
delivered turn → render frame → completion INTENT (never self-ack); throwing
handler → fail intent. Live-stack E2E + manual validation are the OSS-406 proof;
scale-out (Teams, etc.) is OSS-459.