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
Implement ChannelBridge — the central orchestrator that connects incoming channel messages to CLI agent execution and delivery. This is the keystone C1 middleware component that wires together all previously implemented pieces: runtime factory, session map, error classifier, delivery adapter, system prompt builder, and MCP side effects.
Architecture
ChannelMessage (from channel adapter)
|
v
ChannelBridge.handle(message, callbacks?, abortSignal?)
|
├── 1. Session lookup (SessionMap)
├── 2. System prompt construction (buildSystemPrompt)
├── 3. MCP config assembly (McpServerConfig for runtime)
├── 4. Runtime params construction (AgentExecuteParams)
├── 5. Runtime execution (createCliRuntime → execute())
├── 6. Event streaming (DeliveryAdapter + BridgeCallbacks)
├── 7. Error classification (ErrorClassifier)
├── 8. MCP side effects reading (readMcpSideEffects)
├── 9. Session update (SessionMap)
└── 10. Result assembly (AgentDeliveryResult)
|
v
AgentDeliveryResult (to delivery pipeline)
Single Entry Point
ChannelBridge.handle() is the single entry point for all dispatch sites. All 4 core dispatch sites (agent command, auto-reply, cron, follow-up) and the voice-call extension dispatch site will call this method instead of runEmbeddedPiAgent().
Dual Delivery Model
During execution: Streaming callbacks (onPartialReply, onBlockReply, onToolResult) deliver text to the channel in real-time as the CLI agent generates output
After execution: Final AgentDeliveryResult.payloads deliver complete text blocks (for non-streaming fallback, errors, or media)
AgentDeliveryResult, ChannelMessage, BridgeCallbacks, etc.
handle() Flow
asynchandle(message: ChannelMessage,callbacks?: BridgeCallbacks,abortSignal?: AbortSignal,): Promise<AgentDeliveryResult>{// 1. Session lookupconstsessionKey=buildSessionKey(message);constexistingSessionId=this.sessionMap.get(sessionKey);// 2. System promptconstsystemPrompt=buildSystemPrompt({ ... });// 3. MCP config: build McpServerConfig with gateway env varsconstinvocationDir=awaitmkdtemp(join(tmpdir(),"rc-"));constsideEffectsFile=join(invocationDir,"side-effects.ndjson");constmcpServers=this.buildMcpConfig(message,sideEffectsFile);// 4. Runtime paramsconstruntime=createCliRuntime(this.provider);constparams: AgentExecuteParams={prompt: systemPrompt+"\n\n"+message.text,sessionId: existingSessionId,
mcpServers,
abortSignal,workingDirectory: this.workspaceDir,env: this.buildRuntimeEnv(),};// 5-6. Execute + stream events through DeliveryAdapterconstadapter=newDeliveryAdapter({chunkLimit: this.chunkLimit});letrunResult: AgentRunResult|undefined;letlastError: string|undefined;try{const{ payloads, result, error }=awaitadapter.process(runtime.execute(params),callbacks,);runResult=result;lastError=error;}catch(err){// 7. Error classificationconstcategory=classifyError(String(err));lastError=String(err);// Handle based on category (retryable, fatal, context_overflow)}// 8. Read MCP side effectsconstmcp=awaitreadMcpSideEffects(sideEffectsFile);// 9. Session updateif(runResult?.sessionId){this.sessionMap.set(sessionKey,runResult.sessionId);}// 10. Cleanup and returnawaitrm(invocationDir,{recursive: true,force: true});consttext=(runResult?.text??"").trim();return{payloads: text ? [{ text }] : [],run: runResult??DEFAULT_RUN_RESULT,
mcp,error: lastError,};}
Constructor / Configuration
exportclassChannelBridge{constructor(options: {/** CLI runtime provider ("claude", "gemini", "codex", "opencode"). */provider: string;/** Session map for session persistence. */sessionMap: SessionMap;/** Gateway URL for MCP server WebSocket connection. */gatewayUrl: string;/** Gateway auth token for MCP server. */gatewayToken: string;/** Working directory for CLI subprocess. */workspaceDir?: string;/** Channel text chunk limit (default: 4000). */chunkLimit?: number;/** MCP server entry point path. */mcpServerPath?: string;});}
MCP Config Assembly
ChannelBridge builds a McpServerConfig for the remoteclaw MCP server and passes it via AgentExecuteParams.mcpServers. Each runtime already handles format translation internally (Claude: inline JSON flag, Gemini: settings.json merge, Codex: TOML config, OpenCode: JSON config).
No retry loops in ChannelBridge: Error classification is reported, not acted upon. Dispatch sites own retry policy (different for cron vs interactive).
Temp directory per invocation: Each handle() call creates a temp dir for the MCP side effects file. Cleaned up in finally.
Runtime created per call: createCliRuntime() is called each handle(). Runtimes are lightweight (no state between invocations).
DeliveryAdapter owns event-to-payload conversion: ChannelBridge delegates all text chunking and streaming to DeliveryAdapter. It only assembles the final AgentDeliveryResult.
Summary
Implement
ChannelBridge— the central orchestrator that connects incoming channel messages to CLI agent execution and delivery. This is the keystone C1 middleware component that wires together all previously implemented pieces: runtime factory, session map, error classifier, delivery adapter, system prompt builder, and MCP side effects.Architecture
Single Entry Point
ChannelBridge.handle()is the single entry point for all dispatch sites. All 4 core dispatch sites (agent command, auto-reply, cron, follow-up) and the voice-call extension dispatch site will call this method instead ofrunEmbeddedPiAgent().Dual Delivery Model
onPartialReply,onBlockReply,onToolResult) deliver text to the channel in real-time as the CLI agent generates outputAgentDeliveryResult.payloadsdeliver complete text blocks (for non-streaming fallback, errors, or media)Dependencies (all implemented)
runtime-factory.tscreateCliRuntime(provider)error-classifier.tsclassifyError(stderr)for retry/fail decisionssession-map.tsdelivery-adapter.tsAgentEventstream →ReplyPayload[]chunkssystem-prompt.tsbuildSystemPrompt(params)mcp-side-effects.tsreadMcpSideEffects(filePath)after CLI exittypes.tsAgentDeliveryResult,ChannelMessage,BridgeCallbacks, etc.handle()FlowConstructor / Configuration
MCP Config Assembly
ChannelBridge builds a
McpServerConfigfor the remoteclaw MCP server and passes it viaAgentExecuteParams.mcpServers. Each runtime already handles format translation internally (Claude: inline JSON flag, Gemini: settings.json merge, Codex: TOML config, OpenCode: JSON config).Session Key Construction
Composite key encoding channel + user + thread context:
This matches the
SessionMapkey format from SessionMap (#24).Error Handling Strategy
retryableerrorSubtypeon result, let caller decide retryfatalcontext_overflowerrorSubtype: "context_window", caller may start new sessiontimeout/abortedChannelBridge does NOT implement retry loops — it reports the error category to the caller (dispatch site). The dispatch site owns retry policy.
Implementation
New Files
src/middleware/channel-bridge.tsTest Files
src/middleware/channel-bridge.test.tsEstimated total: ~200 LoC + ~250 LoC tests
Key Design Decisions
No retry loops in ChannelBridge: Error classification is reported, not acted upon. Dispatch sites own retry policy (different for cron vs interactive).
Temp directory per invocation: Each
handle()call creates a temp dir for the MCP side effects file. Cleaned up infinally.Runtime created per call:
createCliRuntime()is called eachhandle(). Runtimes are lightweight (no state between invocations).DeliveryAdapter owns event-to-payload conversion: ChannelBridge delegates all text chunking and streaming to DeliveryAdapter. It only assembles the final
AgentDeliveryResult.Session key = channel+user+thread: Matches SessionMap's composite key design from SessionMap (Implement SessionMap #24).
Acceptance Criteria
src/middleware/channel-bridge.tsorchestrates the full message → execution → delivery flowonPartialReply,onBlockReply,onToolResult) are invoked during executionreadMcpSideEffects()after CLI exit (file-based NDJSON per side effects design analysis)errorSubtypeon the resulthandle()method is the single entry point for all dispatch sitespnpm buildpasses