From 41af8557acb7f0590970c0c431e9868dce9f1b52 Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 31 Mar 2026 16:04:36 -0700 Subject: [PATCH 1/2] Clean up paired bridge messaging --- src/loop/bridge-config.ts | 2 +- src/loop/bridge-guidance.ts | 4 +- src/loop/bridge-message-format.ts | 22 ++- src/loop/bridge-runtime.ts | 4 +- src/loop/bridge.ts | 128 ++++++++++---- src/loop/codex-tmux-proxy.ts | 4 +- src/loop/paired-loop.ts | 29 ++-- src/loop/tmux.ts | 6 +- tests/loop/bridge.test.ts | 248 +++++++++++++++++++++++++--- tests/loop/codex-app-server.test.ts | 2 +- tests/loop/codex-tmux-proxy.test.ts | 10 +- tests/loop/paired-loop.test.ts | 14 +- tests/loop/tmux.test.ts | 10 +- 13 files changed, 388 insertions(+), 95 deletions(-) diff --git a/src/loop/bridge-config.ts b/src/loop/bridge-config.ts index 193a7c0..1d8bde1 100644 --- a/src/loop/bridge-config.ts +++ b/src/loop/bridge-config.ts @@ -6,7 +6,7 @@ import { buildLaunchArgv } from "./launch"; import type { Agent } from "./types"; const CODEX_AUTO_APPROVED_BRIDGE_TOOLS = [ - "send_to_agent", + "send_message", "bridge_status", "receive_messages", ] as const; diff --git a/src/loop/bridge-guidance.ts b/src/loop/bridge-guidance.ts index 23b2cf6..3c82c3d 100644 --- a/src/loop/bridge-guidance.ts +++ b/src/loop/bridge-guidance.ts @@ -10,10 +10,10 @@ export const receiveMessagesStuckGuidance = 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.'; export const sendToClaudeGuidance = (): string => - `Use "send_to_agent" with ${bridgeTargetLiteral("claude")} for Claude-facing messages, not a human-facing message.`; + `Use "send_message" with ${bridgeTargetLiteral("claude")} for Claude-facing messages, not a human-facing message.`; export const sendProactiveCodexGuidance = (): string => - `Use "send_to_agent" with ${bridgeTargetLiteral("codex")} for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.`; + `Use "send_message" with ${bridgeTargetLiteral("codex")} for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.`; export const claudeChannelInstructions = (): string => [ diff --git a/src/loop/bridge-message-format.ts b/src/loop/bridge-message-format.ts index 35a6408..a686d27 100644 --- a/src/loop/bridge-message-format.ts +++ b/src/loop/bridge-message-format.ts @@ -1,18 +1,34 @@ import type { Agent } from "./types"; +const BRIDGE_TAG_RE = /<\/?loop-bridge(?:\s+[^>]*)?>/gi; const BRIDGE_PREFIX_RE = /^(?:Message from (?:Claude|Codex) via the loop bridge:|(?:Claude|Codex):)\s*/i; +const bridgeSourceLabel = (source: Agent): string => + source === "claude" ? "Claude" : "Codex"; + export const formatCodexBridgeMessage = ( source: Agent, - message: string + message: string, + messageId?: string ): string => { const trimmed = message.trim(); if (!trimmed) { return ""; } - return source === "claude" ? `Claude: ${trimmed}` : trimmed; + const messageIdAttr = messageId ? ` message_id="${messageId}"` : ""; + return [ + ``, + `${bridgeSourceLabel(source)}: ${trimmed}`, + "", + ].join("\n"); }; export const normalizeBridgeMessage = (message: string): string => - message.trim().replace(BRIDGE_PREFIX_RE, "").replace(/\s+/g, " "); + message + .trim() + .replace(BRIDGE_TAG_RE, " ") + .trim() + .replace(BRIDGE_PREFIX_RE, "") + .replace(/\s+/g, " ") + .trim(); diff --git a/src/loop/bridge-runtime.ts b/src/loop/bridge-runtime.ts index a46df79..2ab5952 100644 --- a/src/loop/bridge-runtime.ts +++ b/src/loop/bridge-runtime.ts @@ -329,7 +329,7 @@ export const deliverCodexBridgeMessage = async ( const delivered = await injectCodexMessage( status.codexRemoteUrl, status.codexThreadId, - formatCodexBridgeMessage(message.source, message.message) + formatCodexBridgeMessage(message.source, message.message, message.id) ); if (delivered) { acknowledgeBridgeDelivery( @@ -361,7 +361,7 @@ export const drainCodexTmuxMessages = async ( } const delivered = await injectCodexTmuxMessage( status.tmuxSession, - formatCodexBridgeMessage(message.source, message.message) + formatCodexBridgeMessage(message.source, message.message, message.id) ); if (!delivered) { return false; diff --git a/src/loop/bridge.ts b/src/loop/bridge.ts index af3dcf2..2372742 100644 --- a/src/loop/bridge.ts +++ b/src/loop/bridge.ts @@ -1,3 +1,5 @@ +import { mkdirSync, watch } from "node:fs"; +import { basename } from "node:path"; import { claudeChannelServerName } from "./bridge-config"; import { BRIDGE_SERVER as BRIDGE_SERVER_VALUE } from "./bridge-constants"; import { @@ -28,8 +30,8 @@ import { import { LOOP_VERSION } from "./constants"; import type { Agent } from "./types"; -const CHANNEL_POLL_DELAY_MS = 500; const CLAUDE_CHANNEL_CAPABILITY = "claude/channel"; +const CLAUDE_CHANNEL_FALLBACK_SWEEP_MS = 2000; const CONTENT_LENGTH_RE = /Content-Length:\s*(\d+)/i; const CONTENT_LENGTH_PREFIX = "content-length:"; const DEFAULT_PROTOCOL_VERSION = "2024-11-05"; @@ -142,7 +144,7 @@ const handleReceiveMessagesTool = ( }); }; -const handleSendToAgentTool = async ( +const handleSendMessageTool = async ( id: JsonRpcRequest["id"], runDir: string, source: Agent, @@ -154,7 +156,7 @@ const handleSendToAgentTool = async ( writeError( id, MCP_INVALID_PARAMS, - "send_to_agent requires a non-empty target" + "send_message requires a non-empty target" ); return; } @@ -171,7 +173,7 @@ const handleSendToAgentTool = async ( writeError( id, MCP_INVALID_PARAMS, - "send_to_agent requires a non-empty message" + "send_message requires a non-empty message" ); return; } @@ -179,7 +181,7 @@ const handleSendToAgentTool = async ( writeError( id, MCP_INVALID_PARAMS, - "send_to_agent cannot target the current agent" + "send_message cannot target the current agent" ); return; } @@ -244,12 +246,21 @@ const handleToolCall = async ( return; } - if (name !== "send_to_agent") { + if (name === "send_to_agent") { + writeError( + id, + MCP_INVALID_PARAMS, + 'Unknown tool: send_to_agent. Use "send_message" instead.' + ); + return; + } + + if (name !== "send_message") { writeError(id, MCP_INVALID_PARAMS, `Unknown tool: ${name}`); return; } - await handleSendToAgentTool(id, runDir, source, args); + await handleSendMessageTool(id, runDir, source, args); }; const requestedProtocolVersion = (request: JsonRpcRequest): string => @@ -312,7 +323,7 @@ const handleBridgeRequest = async ( tools: [ { annotations: MUTATING_TOOL_ANNOTATIONS, - description: "Send an explicit message to the paired agent.", + description: "Send a direct message to the paired agent.", inputSchema: { additionalProperties: false, properties: { @@ -325,12 +336,12 @@ const handleBridgeRequest = async ( required: ["target", "message"], type: "object", }, - name: "send_to_agent", + name: "send_message", }, { annotations: READ_ONLY_TOOL_ANNOTATIONS, description: - "Inspect the current paired run and pending bridge messages.", + "Inspect the current paired run and pending bridge state when delivery looks stuck.", inputSchema: { additionalProperties: false, properties: {}, @@ -341,7 +352,7 @@ const handleBridgeRequest = async ( { annotations: RECEIVE_MESSAGES_TOOL_ANNOTATIONS, description: - "Read and clear pending bridge messages addressed to you.", + "Read and clear pending bridge messages addressed to you when delivery looks stuck.", inputSchema: { additionalProperties: false, properties: {}, @@ -481,12 +492,24 @@ const consumeFrames = ( process.stdin.on("error", reject); }); +const isBridgeWatchEvent = ( + runDir: string, + filename: string | Buffer | null +): boolean => { + if (!filename) { + return true; + } + return filename.toString() === basename(bridgePath(runDir)); +}; + export const runBridgeMcpServer = async ( runDir: string, source: Agent ): Promise => { let channelReady = false; + let bridgeWatcher: { close: () => void } | undefined; let closed = false; + let fallbackSweep: ReturnType | undefined; let flushQueue: Promise = Promise.resolve(); let requestQueue: Promise = Promise.resolve(); const queueClaudeFlush = (): Promise => { @@ -499,39 +522,74 @@ export const runBridgeMcpServer = async ( flushQueue = flushQueue.then(next, next); return flushQueue; }; - const pollClaudeChannel = async (): Promise => { - while (!closed) { - await queueClaudeFlush(); + + const triggerClaudeFlush = (): void => { + queueClaudeFlush().catch(() => undefined); + }; + + const clearClaudeSweep = (): void => { + if (!fallbackSweep) { + return; + } + clearTimeout(fallbackSweep); + fallbackSweep = undefined; + }; + + const scheduleClaudeSweep = (): void => { + if (!(source === "claude" && channelReady) || closed || fallbackSweep) { + return; + } + fallbackSweep = setTimeout(() => { + fallbackSweep = undefined; if (closed) { return; } - await new Promise((resolve) => { - setTimeout(resolve, CHANNEL_POLL_DELAY_MS); - }); - } + triggerClaudeFlush(); + scheduleClaudeSweep(); + }, CLAUDE_CHANNEL_FALLBACK_SWEEP_MS); + fallbackSweep.unref?.(); }; - process.stdin.resume(); - const poller = source === "claude" ? pollClaudeChannel() : Promise.resolve(); - await consumeFrames( - (request) => { - const handleRequest = async (): Promise => { - if (request.method === "notifications/initialized") { - channelReady = true; + if (source === "claude") { + mkdirSync(runDir, { recursive: true }); + try { + bridgeWatcher = watch(runDir, (_eventType, filename) => { + if (!isBridgeWatchEvent(runDir, filename)) { + return; } - await handleBridgeRequest(runDir, source, request); - await queueClaudeFlush(); - }; - requestQueue = requestQueue.then(handleRequest, handleRequest); - }, - () => { - closed = true; + triggerClaudeFlush(); + }); + } catch { + bridgeWatcher = undefined; } - ); - closed = true; + } + + process.stdin.resume(); + try { + await consumeFrames( + (request) => { + const handleRequest = async (): Promise => { + if (request.method === "notifications/initialized") { + channelReady = true; + scheduleClaudeSweep(); + } + await handleBridgeRequest(runDir, source, request); + await queueClaudeFlush(); + }; + requestQueue = requestQueue.then(handleRequest, handleRequest); + }, + () => { + closed = true; + } + ); + } finally { + closed = true; + bridgeWatcher?.close(); + clearClaudeSweep(); + } + await requestQueue; await queueClaudeFlush(); - await poller; }; export const bridgeInternals = { diff --git a/src/loop/codex-tmux-proxy.ts b/src/loop/codex-tmux-proxy.ts index 25800f9..aaa14d2 100644 --- a/src/loop/codex-tmux-proxy.ts +++ b/src/loop/codex-tmux-proxy.ts @@ -235,7 +235,7 @@ const buildBridgeInjectionFrame = ( params: { expectedTurnId: activeTurnId, input: buildInput( - formatCodexBridgeMessage(message.source, message.message) + formatCodexBridgeMessage(message.source, message.message, message.id) ), threadId, }, @@ -246,7 +246,7 @@ const buildBridgeInjectionFrame = ( method: TURN_START_METHOD, params: { input: buildInput( - formatCodexBridgeMessage(message.source, message.message) + formatCodexBridgeMessage(message.source, message.message, message.id) ), threadId, }, diff --git a/src/loop/paired-loop.ts b/src/loop/paired-loop.ts index 0c5a414..6c8b3db 100644 --- a/src/loop/paired-loop.ts +++ b/src/loop/paired-loop.ts @@ -100,14 +100,15 @@ const bridgeGuidance = (agent: Agent): string => { const target = agent === "claude" ? "codex" : "claude"; return [ "Paired mode:", - `You are in a persistent Claude/Codex pair. Use the MCP tool "send_to_agent" with ${bridgeTargetLiteral(target)} when you want ${peer} to act, review, or answer.`, - 'Do not ask the human to relay messages between agents or answer the human on the other agent\'s behalf. Use "bridge_status" if you need the current bridge state.', - 'If "bridge_status" shows pending messages addressed to you, call "receive_messages" to read them.', + `You are in a persistent Claude/Codex pair. Use the MCP tool "send_message" with ${bridgeTargetLiteral(target)} when you want ${peer} to act, review, or answer.`, + 'Do not ask the human to relay messages between agents or answer the human on the other agent\'s behalf. Use "bridge_status" only if delivery looks stuck.', + 'Use "receive_messages" only if "bridge_status" shows pending messages addressed to you and direct delivery looks stuck.', ].join("\n"); }; const bridgeToolGuidance = [ - 'You can use the MCP tools "send_to_agent", "bridge_status", and "receive_messages" for direct Claude/Codex coordination.', + 'You can use the MCP tools "send_message", "bridge_status", and "receive_messages" for direct Claude/Codex coordination.', + 'Only use "bridge_status" or "receive_messages" when delivery looks stuck.', "Do not ask the human to relay messages between agents.", ].join("\n"); @@ -116,7 +117,7 @@ const reviewDeliveryGuidance = (reviewer: Agent, opts: Options): string => { return "If review is needed, keep the actionable notes in your review body before the final review signal."; } - return `If review is needed, send the actionable notes to ${capitalize(opts.agent)} with "send_to_agent" using ${bridgeTargetLiteral(opts.agent)} before returning your final review signal.`; + return `If review is needed, send the actionable notes to ${capitalize(opts.agent)} with "send_message" using ${bridgeTargetLiteral(opts.agent)} before returning your final review signal.`; }; const reviewToolGuidance = (reviewer: Agent, opts: Options): string => @@ -151,19 +152,27 @@ const reviewBridgePrompt = ( .filter(Boolean) .join("\n\n"); -const forwardBridgePrompt = (source: Agent, message: string): string => +const forwardBridgePrompt = ({ + id, + message, + source, +}: { + id: string; + message: string; + source: Agent; +}): string => (source === "claude" ? [ - formatCodexBridgeMessage(source, message), + formatCodexBridgeMessage(source, message, id), "Treat this as direct agent-to-agent coordination. Do not reply to the human.", - 'Send a message to the other agent with "send_to_agent" only when you have something useful for them to act on.', + 'Send a message to the other agent with "send_message" only when you have something useful for them to act on.', "Do not acknowledge receipt without new information.", ] : [ `Message from ${capitalize(source)} via the loop bridge:`, message.trim(), "Treat this as direct agent-to-agent coordination. Do not reply to the human.", - 'Send a message to the other agent with "send_to_agent" only when you have something useful for them to act on.', + 'Send a message to the other agent with "send_message" only when you have something useful for them to act on.', "Do not acknowledge receipt without new information.", ] ).join("\n\n"); @@ -284,7 +293,7 @@ const drainBridge = async ( const result = await tryRunPairedAgent( state, message.target, - forwardBridgePrompt(message.source, message.message) + forwardBridgePrompt(message) ); if (!result) { return { deliveredToPrimary }; diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index a456be0..7df54a4 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -202,14 +202,14 @@ const buildPrimaryPrompt = ( const parts = [ `Agent-to-agent pair programming: you are the primary ${capitalize(opts.agent)} agent for this run.`, `Task:\n${task.trim()}`, - `Your peer is ${peer}. Do the initial pass yourself, then use "send_to_agent" when you want review or targeted help from ${peer}.`, + `Your peer is ${peer}. Do the initial pass yourself, then use "send_message" when you want review or targeted help from ${peer}.`, ]; appendProofPrompt(parts, opts.proof); parts.push(SPAWN_TEAM_WITH_WORKTREE_ISOLATION); parts.push(pairedBridgeGuidance(opts.agent, runId, serverName)); parts.push(pairedWorkflowGuidance(opts, opts.agent)); parts.push( - `${peer} should send a short ready message. Wait briefly if it arrives, then inspect the repo and start. Ask ${peer} for review once you have concrete work or a specific question.` + `Inspect the repo and start. Ask ${peer} for review once you have concrete work or a specific question.` ); return parts.join("\n\n"); }; @@ -245,7 +245,7 @@ const buildInteractivePrimaryPrompt = ( const parts = [ `Agent-to-agent pair programming: you are the primary ${capitalize(opts.agent)} agent for this run.`, "No task has been assigned yet.", - `Your peer is ${peer}. Use "send_to_agent" for review or help once the human gives you a task.`, + `Your peer is ${peer}. Use "send_message" for review or help once the human gives you a task.`, ]; appendProofPrompt(parts, opts.proof); parts.push( diff --git a/tests/loop/bridge.test.ts b/tests/loop/bridge.test.ts index c26ad88..8ba70e1 100644 --- a/tests/loop/bridge.test.ts +++ b/tests/loop/bridge.test.ts @@ -75,6 +75,16 @@ const toolText = (stdout: string, id: number): string => { )?.content; return content?.[0]?.text ?? ""; }; +const codexBridgeEnvelope = ( + id: string, + message: string, + source: "claude" | "codex" = "claude" +): string => + [ + ``, + `${source === "claude" ? "Claude" : "Codex"}: ${message}`, + "", + ].join("\n"); const runBridgeProcess = async ( runDir: string, @@ -107,6 +117,59 @@ const runBridgeProcess = async ( return { code, stderr, stdout }; }; +const startLiveBridgeProcess = ( + runDir: string, + source: "claude" | "codex", + env?: NodeJS.ProcessEnv +): { + close: () => Promise<{ code: number | null; stderr: string; stdout: string }>; + write: (frame: string) => void; + waitForStdout: (pattern: string, timeoutMs?: number) => Promise; +} => { + const cli = join(process.cwd(), "src", "cli.ts"); + const child = spawn(process.execPath, [cli, "__bridge-mcp", runDir, source], { + cwd: process.cwd(), + env, + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { + stdout += chunk; + }); + child.stderr.setEncoding("utf8"); + child.stderr.on("data", (chunk: string) => { + stderr += chunk; + }); + + return { + close: async () => { + child.stdin.end(); + const code = await new Promise((resolve) => { + child.on("close", resolve); + }); + return { code, stderr, stdout }; + }, + write: (frame) => { + child.stdin.write(frame); + }, + waitForStdout: async (pattern, timeoutMs = 5000) => { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (stdout.includes(pattern)) { + return; + } + await new Promise((resolve) => { + setTimeout(resolve, 25); + }); + } + throw new Error(`timed out waiting for stdout to contain: ${pattern}`); + }, + }; +}; + afterEach(() => { mock.restore(); }); @@ -418,11 +481,19 @@ test("bridge normalization treats short and legacy Claude prefixes as equivalent "Claude: Please verify the final diff." ) ).toBe(true); + expect( + bridge.blocksBridgeBounce( + runDir, + "codex", + "claude", + codexBridgeEnvelope("msg-2", "Please verify the final diff.") + ) + ).toBe(true); rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent queues a direct message through the CLI path", async () => { +test("bridge MCP send_message queues a direct message through the CLI path", async () => { const bridge = await loadBridge(); const root = makeTempDir(); const runDir = join(root, "run"); @@ -441,7 +512,7 @@ test("bridge MCP send_to_agent queues a direct message through the CLI path", as message: "ship it", target: "codex", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -466,7 +537,7 @@ test("bridge MCP send_to_agent queues a direct message through the CLI path", as rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent normalizes target case and whitespace", async () => { +test("bridge MCP send_message normalizes target case and whitespace", async () => { const bridge = await loadBridge(); const root = makeTempDir(); const runDir = join(root, "run"); @@ -485,7 +556,7 @@ test("bridge MCP send_to_agent normalizes target case and whitespace", async () message: "ship it", target: " CLAUDE ", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -505,7 +576,7 @@ test("bridge MCP send_to_agent normalizes target case and whitespace", async () rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent rejects an empty target after trimming", async () => { +test("bridge MCP send_message rejects an empty target after trimming", async () => { const root = makeTempDir(); const runDir = join(root, "run"); mkdirSync(runDir, { recursive: true }); @@ -523,7 +594,7 @@ test("bridge MCP send_to_agent rejects an empty target after trimming", async () message: "ship it", target: " ", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -534,7 +605,7 @@ test("bridge MCP send_to_agent rejects an empty target after trimming", async () expect(JSON.parse(result.stdout)).toMatchObject({ error: { code: -32_602, - message: "send_to_agent requires a non-empty target", + message: "send_message requires a non-empty target", }, id: 1, jsonrpc: "2.0", @@ -542,7 +613,7 @@ test("bridge MCP send_to_agent rejects an empty target after trimming", async () rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent rejects an unknown normalized target", async () => { +test("bridge MCP send_message rejects an unknown normalized target", async () => { const root = makeTempDir(); const runDir = join(root, "run"); mkdirSync(runDir, { recursive: true }); @@ -560,7 +631,7 @@ test("bridge MCP send_to_agent rejects an unknown normalized target", async () = message: "ship it", target: " FOO ", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -579,7 +650,7 @@ test("bridge MCP send_to_agent rejects an unknown normalized target", async () = rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent rejects targeting the current agent", async () => { +test("bridge MCP send_message rejects targeting the current agent", async () => { const root = makeTempDir(); const runDir = join(root, "run"); mkdirSync(runDir, { recursive: true }); @@ -597,6 +668,43 @@ test("bridge MCP send_to_agent rejects targeting the current agent", async () => message: "ship it", target: "claude", }, + name: "send_message", + }, + }), + "\n", + ].join("") + ); + + expect(result.code).toBe(0); + expect(JSON.parse(result.stdout)).toMatchObject({ + error: { + code: -32_602, + message: "send_message cannot target the current agent", + }, + id: 1, + jsonrpc: "2.0", + }); + rmSync(root, { recursive: true, force: true }); +}); + +test("bridge MCP rejects the old send_to_agent name with rename guidance", async () => { + const root = makeTempDir(); + const runDir = join(root, "run"); + mkdirSync(runDir, { recursive: true }); + + const result = await runBridgeProcess( + runDir, + "claude", + [ + encodeFrame({ + id: 1, + jsonrpc: "2.0", + method: "tools/call", + params: { + arguments: { + message: "ship it", + target: "codex", + }, name: "send_to_agent", }, }), @@ -608,7 +716,7 @@ test("bridge MCP send_to_agent rejects targeting the current agent", async () => expect(JSON.parse(result.stdout)).toMatchObject({ error: { code: -32_602, - message: "send_to_agent cannot target the current agent", + message: 'Unknown tool: send_to_agent. Use "send_message" instead.', }, id: 1, jsonrpc: "2.0", @@ -670,7 +778,7 @@ test("bridge MCP handles standard empty-list and ping requests through the Claud expect(result.stderr).toBe(""); expect(result.stdout).toContain('"claude/channel":{}'); expect(result.stdout).toContain( - '\\"send_to_agent\\" with target: \\"codex\\" for Codex-facing messages' + '\\"send_message\\" with target: \\"codex\\" for Codex-facing messages' ); expect(result.stdout).toContain( "Never answer the human when the inbound message came from Codex" @@ -693,7 +801,7 @@ test("bridge MCP handles standard empty-list and ping requests through the Claud openWorldHint: false, readOnlyHint: false, }, - name: "send_to_agent", + name: "send_message", }), expect.objectContaining({ annotations: { @@ -757,7 +865,7 @@ test("bridge MCP advertises only the Codex-visible bridge tools", async () => { openWorldHint: false, readOnlyHint: false, }, - name: "send_to_agent", + name: "send_message", }), expect.objectContaining({ annotations: { @@ -1073,7 +1181,7 @@ test("bridge delivers Claude replies directly to Codex when app-server state is expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - "Claude: The files look good to me." + codexBridgeEnvelope("msg-1", "The files look good to me.") ); expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); expect( @@ -1131,7 +1239,7 @@ test("bridge prefers Codex app-server delivery even when tmux is live", async () expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - "Claude: Please steer this into the active turn." + codexBridgeEnvelope("msg-live", "Please steer this into the active turn.") ); expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); expect( @@ -1192,7 +1300,7 @@ test("bridge falls back to direct Codex delivery when the stored tmux session is expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - "Claude: Please review the final state." + '\nClaude: Please review the final state.\n' ); expect(readRunManifest(join(runDir, "manifest.json"))?.tmuxSession).toBe( undefined @@ -1277,6 +1385,22 @@ test("bridge drains pending codex tmux messages through the injected command dep ["tmux", "capture-pane", "-p", "-t", "repo-loop-8:0.1"], { stderr: "ignore", stdout: "pipe" }, ], + [ + [ + "tmux", + "send-keys", + "-t", + "repo-loop-8:0.1", + "-l", + "--", + '', + ], + { stderr: "ignore" }, + ], + [ + ["tmux", "send-keys", "-t", "repo-loop-8:0.1", "C-j"], + { stderr: "ignore" }, + ], [ [ "tmux", @@ -1289,6 +1413,22 @@ test("bridge drains pending codex tmux messages through the injected command dep ], { stderr: "ignore" }, ], + [ + ["tmux", "send-keys", "-t", "repo-loop-8:0.1", "C-j"], + { stderr: "ignore" }, + ], + [ + [ + "tmux", + "send-keys", + "-t", + "repo-loop-8:0.1", + "-l", + "--", + "", + ], + { stderr: "ignore" }, + ], [ ["tmux", "send-keys", "-t", "repo-loop-8:0.1", "Enter"], { stderr: "ignore" }, @@ -1592,7 +1732,10 @@ test("runBridgeWorker falls back to app-server delivery after stale tmux cleanup expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - "Claude: Please deliver this after tmux cleanup." + codexBridgeEnvelope( + "msg-stale-fallback", + "Please deliver this after tmux cleanup." + ) ); expect(readRunManifest(join(runDir, "manifest.json"))?.tmuxSession).toBe( undefined @@ -1704,12 +1847,12 @@ test("runBridgeWorker retries queued codex app-server messages", async () => { [ "ws://127.0.0.1:4500", "codex-thread-1", - "Claude: Please review the final diff.", + codexBridgeEnvelope("msg-4", "Please review the final diff."), ], [ "ws://127.0.0.1:4500", "codex-thread-1", - "Claude: Please review the final diff.", + codexBridgeEnvelope("msg-4", "Please review the final diff."), ], ]); expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); @@ -1776,6 +1919,61 @@ test("bridge MCP delivers pending codex messages to Claude as channel notificati rmSync(root, { recursive: true, force: true }); }); +test("bridge MCP flushes new Claude channel messages after bridge file changes", async () => { + const bridge = await loadBridge(); + const root = makeTempDir(); + const runDir = join(root, "run"); + mkdirSync(runDir, { recursive: true }); + const process = startLiveBridgeProcess(runDir, "claude"); + + process.write( + encodeLine({ + id: 1, + jsonrpc: "2.0", + method: "initialize", + params: { + protocolVersion: "2024-11-05", + }, + }) + ); + process.write( + encodeLine({ + jsonrpc: "2.0", + method: "notifications/initialized", + params: {}, + }) + ); + await process.waitForStdout('"id":1'); + + writeFileSync( + bridge.bridgeInternals.bridgePath(runDir), + `${JSON.stringify({ + at: "2026-03-23T10:00:00.000Z", + id: "msg-watch-1", + kind: "message", + message: "Please review the follow-up change.", + source: "codex", + target: "claude", + })}\n`, + "utf8" + ); + + await process.waitForStdout("Please review the follow-up change."); + const result = await process.close(); + + expect(result.code).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain('"method":"notifications/claude/channel"'); + expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); + expect( + bridge.bridgeInternals + .readBridgeEvents(runDir) + .filter((event) => event.kind === "delivered") + ).toHaveLength(1); + + rmSync(root, { recursive: true, force: true }); +}); + test("bridge MCP blocks an immediate bounce from the paired agent", async () => { const bridge = await loadBridge(); const root = makeTempDir(); @@ -1819,7 +2017,7 @@ test("bridge MCP blocks an immediate bounce from the paired agent", async () => message: "ship it", target: "claude", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -1905,7 +2103,7 @@ test("bridge MCP allows the same message later after unrelated traffic", async ( message: "ship it", target: "claude", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -1971,7 +2169,7 @@ test("bridge MCP allows repeating the same message in the original direction", a message: "ship it", target: "codex", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -2017,7 +2215,7 @@ test("bridge config helper builds the bridge MCP entry point for Codex", async ( "codex", ])}`, "-c", - 'mcp_servers.loop-bridge.tools.send_to_agent.approval_mode="approve"', + 'mcp_servers.loop-bridge.tools.send_message.approval_mode="approve"', "-c", 'mcp_servers.loop-bridge.tools.bridge_status.approval_mode="approve"', "-c", @@ -2117,7 +2315,7 @@ test("dispatchBridgeMessage reports delivered when direct codex delivery succeed expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - "Claude: Please review the final diff." + expect.stringContaining("Claude: Please review the final diff.") ); expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); diff --git a/tests/loop/codex-app-server.test.ts b/tests/loop/codex-app-server.test.ts index d990dd1..808437e 100644 --- a/tests/loop/codex-app-server.test.ts +++ b/tests/loop/codex-app-server.test.ts @@ -357,7 +357,7 @@ test("startAppServer normalizes codex bridge config args before spawning", async "-c", expect.stringContaining("mcp_servers.loop-bridge.args="), "-c", - 'mcp_servers.loop-bridge.tools.send_to_agent.approval_mode="approve"', + 'mcp_servers.loop-bridge.tools.send_message.approval_mode="approve"', "-c", 'mcp_servers.loop-bridge.tools.bridge_status.approval_mode="approve"', "-c", diff --git a/tests/loop/codex-tmux-proxy.test.ts b/tests/loop/codex-tmux-proxy.test.ts index 0b5093e..75d646d 100644 --- a/tests/loop/codex-tmux-proxy.test.ts +++ b/tests/loop/codex-tmux-proxy.test.ts @@ -28,6 +28,12 @@ const bridgeMessage = { source: "claude" as const, target: "codex" as const, }; +const codexBridgeEnvelope = (id: string, message: string): string => + [ + ``, + `Claude: ${message}`, + "", + ].join("\n"); const TEST_PORT_RANGE = 200; const TEST_PORT_RETRY_LIMIT = 5; @@ -215,7 +221,7 @@ test("codex tmux proxy steers bridge messages into an active turn", () => { expectedTurnId: "turn-active", input: [ { - text: "Claude: Please review the latest diff.", + text: codexBridgeEnvelope("msg-1", "Please review the latest diff."), text_elements: [], type: "text", }, @@ -238,7 +244,7 @@ test("codex tmux proxy starts a new turn when no active turn exists", () => { params: { input: [ { - text: "Claude: Please review the latest diff.", + text: codexBridgeEnvelope("msg-1", "Please review the latest diff."), text_elements: [], type: "text", }, diff --git a/tests/loop/paired-loop.test.ts b/tests/loop/paired-loop.test.ts index 550b8bf..afef54e 100644 --- a/tests/loop/paired-loop.test.ts +++ b/tests/loop/paired-loop.test.ts @@ -422,7 +422,7 @@ test("preparePairedOptions loads stored pair ids before planning", async () => { expect.arrayContaining([ expect.stringContaining("mcp_servers.loop-bridge.command="), expect.stringContaining("mcp_servers.loop-bridge.args="), - 'mcp_servers.loop-bridge.tools.send_to_agent.approval_mode="approve"', + 'mcp_servers.loop-bridge.tools.send_message.approval_mode="approve"', 'mcp_servers.loop-bridge.tools.bridge_status.approval_mode="approve"', 'mcp_servers.loop-bridge.tools.receive_messages.approval_mode="approve"', ]) @@ -610,7 +610,7 @@ test("runPairedLoop delivers forwarded bridge messages to the target agent", asy expect(calls[0]?.prompt).toContain("Please review the Codex output."); expect(calls[0]?.prompt).toContain("Do not reply to the human."); expect(calls[0]?.prompt).toContain( - 'Send a message to the other agent with "send_to_agent"' + 'Send a message to the other agent with "send_message"' ); const events = bridgeInternals.readBridgeEvents(runDir); @@ -772,6 +772,9 @@ test("runPairedLoop delivers peer messages back to the primary agent", async () expect(calls).toHaveLength(3); expect(calls[0]?.agent).toBe("claude"); expect(calls[1]?.agent).toBe("codex"); + expect(calls[1]?.prompt).toContain( + '' + ); expect(calls[1]?.prompt).toContain("Claude: Please verify"); expect(calls[1]?.prompt).toContain( "Please verify the implementation details." @@ -785,7 +788,7 @@ test("runPairedLoop delivers peer messages back to the primary agent", async () "Found one change to make before landing this." ); expect(calls[2]?.prompt).toContain( - 'Send a message to the other agent with "send_to_agent"' + 'Send a message to the other agent with "send_message"' ); }); }); @@ -814,6 +817,9 @@ test("runPairedLoop skips the default work turn after draining input for the pri expect(calls).toHaveLength(1); expect(calls[0]?.agent).toBe("codex"); + expect(calls[0]?.prompt).toContain( + '' + ); expect(calls[0]?.prompt).toContain("Claude: Please verify"); expect(calls[0]?.prompt).toContain( "Please verify the implementation details." @@ -862,7 +868,7 @@ test("runPairedLoop preserves claudex reviewers in paired mode", async () => { "concrete file paths, commands, and code locations that must change" ); expect(reviewPrompts[1]?.prompt).toContain( - 'send the actionable notes to Claude with "send_to_agent" using target: "claude"' + 'send the actionable notes to Claude with "send_message" using target: "claude"' ); }); }); diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index 6987ada..697d17e 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -791,9 +791,9 @@ test("tmux prompts keep the paired review workflow explicit", () => { expect(primaryPrompt).toContain( "create a draft PR or send a follow-up commit to the existing PR" ); - expect(primaryPrompt).toContain("Wait briefly if it arrives"); + expect(primaryPrompt).not.toContain("Wait briefly if it arrives"); expect(primaryPrompt).toContain( - 'Use "send_to_agent" with target: "claude" for Claude-facing messages' + 'Use "send_message" with target: "claude" for Claude-facing messages' ); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("You are the reviewer/support agent."); @@ -801,7 +801,7 @@ test("tmux prompts keep the paired review workflow explicit", () => { expect(peerPrompt).toContain("Wait for Codex to send you a targeted request"); expect(peerPrompt).not.toContain('"reply"'); expect(peerPrompt).toContain( - 'Use "send_to_agent" with target: "codex" for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.' + 'Use "send_message" with target: "codex" for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.' ); expect(primaryPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix"); expect(peerPrompt).toContain("mcp__loop-bridge-repo-123-1__ prefix"); @@ -832,7 +832,7 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { expect(primaryPrompt).toContain("ask Claude for a plan review"); expect(primaryPrompt).toContain("ask the human to review the plan"); expect(primaryPrompt).toContain( - 'Use "send_to_agent" with target: "claude" for Claude-facing messages' + 'Use "send_message" with target: "claude" for Claude-facing messages' ); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("No task has been assigned yet."); @@ -843,7 +843,7 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { expect(peerPrompt).toContain("human clearly assigns you separate work"); expect(peerPrompt).not.toContain('"reply"'); expect(peerPrompt).toContain( - 'Use "send_to_agent" with target: "codex" for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.' + 'Use "send_message" with target: "codex" for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.' ); expect(peerPrompt).toContain( "If you are answering Codex, use the bridge tools instead of a human-facing reply." From 075bdde45958523088979dc59438d524257b24cd Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 31 Mar 2026 16:15:35 -0700 Subject: [PATCH 2/2] Drop bridge message envelope --- src/loop/bridge-message-format.ts | 22 ++-------- src/loop/bridge-runtime.ts | 4 +- src/loop/codex-tmux-proxy.ts | 4 +- src/loop/paired-loop.ts | 4 +- tests/loop/bridge.test.ts | 66 +++-------------------------- tests/loop/codex-tmux-proxy.test.ts | 10 +---- tests/loop/paired-loop.test.ts | 6 --- 7 files changed, 16 insertions(+), 100 deletions(-) diff --git a/src/loop/bridge-message-format.ts b/src/loop/bridge-message-format.ts index a686d27..00ef6cc 100644 --- a/src/loop/bridge-message-format.ts +++ b/src/loop/bridge-message-format.ts @@ -1,34 +1,18 @@ import type { Agent } from "./types"; -const BRIDGE_TAG_RE = /<\/?loop-bridge(?:\s+[^>]*)?>/gi; const BRIDGE_PREFIX_RE = /^(?:Message from (?:Claude|Codex) via the loop bridge:|(?:Claude|Codex):)\s*/i; -const bridgeSourceLabel = (source: Agent): string => - source === "claude" ? "Claude" : "Codex"; - export const formatCodexBridgeMessage = ( source: Agent, - message: string, - messageId?: string + message: string ): string => { const trimmed = message.trim(); if (!trimmed) { return ""; } - const messageIdAttr = messageId ? ` message_id="${messageId}"` : ""; - return [ - ``, - `${bridgeSourceLabel(source)}: ${trimmed}`, - "", - ].join("\n"); + return source === "claude" ? `Claude: ${trimmed}` : trimmed; }; export const normalizeBridgeMessage = (message: string): string => - message - .trim() - .replace(BRIDGE_TAG_RE, " ") - .trim() - .replace(BRIDGE_PREFIX_RE, "") - .replace(/\s+/g, " ") - .trim(); + message.trim().replace(BRIDGE_PREFIX_RE, "").replace(/\s+/g, " ").trim(); diff --git a/src/loop/bridge-runtime.ts b/src/loop/bridge-runtime.ts index 2ab5952..a46df79 100644 --- a/src/loop/bridge-runtime.ts +++ b/src/loop/bridge-runtime.ts @@ -329,7 +329,7 @@ export const deliverCodexBridgeMessage = async ( const delivered = await injectCodexMessage( status.codexRemoteUrl, status.codexThreadId, - formatCodexBridgeMessage(message.source, message.message, message.id) + formatCodexBridgeMessage(message.source, message.message) ); if (delivered) { acknowledgeBridgeDelivery( @@ -361,7 +361,7 @@ export const drainCodexTmuxMessages = async ( } const delivered = await injectCodexTmuxMessage( status.tmuxSession, - formatCodexBridgeMessage(message.source, message.message, message.id) + formatCodexBridgeMessage(message.source, message.message) ); if (!delivered) { return false; diff --git a/src/loop/codex-tmux-proxy.ts b/src/loop/codex-tmux-proxy.ts index aaa14d2..25800f9 100644 --- a/src/loop/codex-tmux-proxy.ts +++ b/src/loop/codex-tmux-proxy.ts @@ -235,7 +235,7 @@ const buildBridgeInjectionFrame = ( params: { expectedTurnId: activeTurnId, input: buildInput( - formatCodexBridgeMessage(message.source, message.message, message.id) + formatCodexBridgeMessage(message.source, message.message) ), threadId, }, @@ -246,7 +246,7 @@ const buildBridgeInjectionFrame = ( method: TURN_START_METHOD, params: { input: buildInput( - formatCodexBridgeMessage(message.source, message.message, message.id) + formatCodexBridgeMessage(message.source, message.message) ), threadId, }, diff --git a/src/loop/paired-loop.ts b/src/loop/paired-loop.ts index 6c8b3db..e0f9bf4 100644 --- a/src/loop/paired-loop.ts +++ b/src/loop/paired-loop.ts @@ -153,17 +153,15 @@ const reviewBridgePrompt = ( .join("\n\n"); const forwardBridgePrompt = ({ - id, message, source, }: { - id: string; message: string; source: Agent; }): string => (source === "claude" ? [ - formatCodexBridgeMessage(source, message, id), + formatCodexBridgeMessage(source, message), "Treat this as direct agent-to-agent coordination. Do not reply to the human.", 'Send a message to the other agent with "send_message" only when you have something useful for them to act on.', "Do not acknowledge receipt without new information.", diff --git a/tests/loop/bridge.test.ts b/tests/loop/bridge.test.ts index 8ba70e1..f716b79 100644 --- a/tests/loop/bridge.test.ts +++ b/tests/loop/bridge.test.ts @@ -75,16 +75,6 @@ const toolText = (stdout: string, id: number): string => { )?.content; return content?.[0]?.text ?? ""; }; -const codexBridgeEnvelope = ( - id: string, - message: string, - source: "claude" | "codex" = "claude" -): string => - [ - ``, - `${source === "claude" ? "Claude" : "Codex"}: ${message}`, - "", - ].join("\n"); const runBridgeProcess = async ( runDir: string, @@ -481,15 +471,6 @@ test("bridge normalization treats short and legacy Claude prefixes as equivalent "Claude: Please verify the final diff." ) ).toBe(true); - expect( - bridge.blocksBridgeBounce( - runDir, - "codex", - "claude", - codexBridgeEnvelope("msg-2", "Please verify the final diff.") - ) - ).toBe(true); - rmSync(root, { recursive: true, force: true }); }); @@ -1181,7 +1162,7 @@ test("bridge delivers Claude replies directly to Codex when app-server state is expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - codexBridgeEnvelope("msg-1", "The files look good to me.") + "Claude: The files look good to me." ); expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); expect( @@ -1239,7 +1220,7 @@ test("bridge prefers Codex app-server delivery even when tmux is live", async () expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - codexBridgeEnvelope("msg-live", "Please steer this into the active turn.") + "Claude: Please steer this into the active turn." ); expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); expect( @@ -1300,7 +1281,7 @@ test("bridge falls back to direct Codex delivery when the stored tmux session is expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - '\nClaude: Please review the final state.\n' + "Claude: Please review the final state." ); expect(readRunManifest(join(runDir, "manifest.json"))?.tmuxSession).toBe( undefined @@ -1385,22 +1366,6 @@ test("bridge drains pending codex tmux messages through the injected command dep ["tmux", "capture-pane", "-p", "-t", "repo-loop-8:0.1"], { stderr: "ignore", stdout: "pipe" }, ], - [ - [ - "tmux", - "send-keys", - "-t", - "repo-loop-8:0.1", - "-l", - "--", - '', - ], - { stderr: "ignore" }, - ], - [ - ["tmux", "send-keys", "-t", "repo-loop-8:0.1", "C-j"], - { stderr: "ignore" }, - ], [ [ "tmux", @@ -1413,22 +1378,6 @@ test("bridge drains pending codex tmux messages through the injected command dep ], { stderr: "ignore" }, ], - [ - ["tmux", "send-keys", "-t", "repo-loop-8:0.1", "C-j"], - { stderr: "ignore" }, - ], - [ - [ - "tmux", - "send-keys", - "-t", - "repo-loop-8:0.1", - "-l", - "--", - "", - ], - { stderr: "ignore" }, - ], [ ["tmux", "send-keys", "-t", "repo-loop-8:0.1", "Enter"], { stderr: "ignore" }, @@ -1732,10 +1681,7 @@ test("runBridgeWorker falls back to app-server delivery after stale tmux cleanup expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - codexBridgeEnvelope( - "msg-stale-fallback", - "Please deliver this after tmux cleanup." - ) + "Claude: Please deliver this after tmux cleanup." ); expect(readRunManifest(join(runDir, "manifest.json"))?.tmuxSession).toBe( undefined @@ -1847,12 +1793,12 @@ test("runBridgeWorker retries queued codex app-server messages", async () => { [ "ws://127.0.0.1:4500", "codex-thread-1", - codexBridgeEnvelope("msg-4", "Please review the final diff."), + "Claude: Please review the final diff.", ], [ "ws://127.0.0.1:4500", "codex-thread-1", - codexBridgeEnvelope("msg-4", "Please review the final diff."), + "Claude: Please review the final diff.", ], ]); expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); diff --git a/tests/loop/codex-tmux-proxy.test.ts b/tests/loop/codex-tmux-proxy.test.ts index 75d646d..0b5093e 100644 --- a/tests/loop/codex-tmux-proxy.test.ts +++ b/tests/loop/codex-tmux-proxy.test.ts @@ -28,12 +28,6 @@ const bridgeMessage = { source: "claude" as const, target: "codex" as const, }; -const codexBridgeEnvelope = (id: string, message: string): string => - [ - ``, - `Claude: ${message}`, - "", - ].join("\n"); const TEST_PORT_RANGE = 200; const TEST_PORT_RETRY_LIMIT = 5; @@ -221,7 +215,7 @@ test("codex tmux proxy steers bridge messages into an active turn", () => { expectedTurnId: "turn-active", input: [ { - text: codexBridgeEnvelope("msg-1", "Please review the latest diff."), + text: "Claude: Please review the latest diff.", text_elements: [], type: "text", }, @@ -244,7 +238,7 @@ test("codex tmux proxy starts a new turn when no active turn exists", () => { params: { input: [ { - text: codexBridgeEnvelope("msg-1", "Please review the latest diff."), + text: "Claude: Please review the latest diff.", text_elements: [], type: "text", }, diff --git a/tests/loop/paired-loop.test.ts b/tests/loop/paired-loop.test.ts index afef54e..a0a13ab 100644 --- a/tests/loop/paired-loop.test.ts +++ b/tests/loop/paired-loop.test.ts @@ -772,9 +772,6 @@ test("runPairedLoop delivers peer messages back to the primary agent", async () expect(calls).toHaveLength(3); expect(calls[0]?.agent).toBe("claude"); expect(calls[1]?.agent).toBe("codex"); - expect(calls[1]?.prompt).toContain( - '' - ); expect(calls[1]?.prompt).toContain("Claude: Please verify"); expect(calls[1]?.prompt).toContain( "Please verify the implementation details." @@ -817,9 +814,6 @@ test("runPairedLoop skips the default work turn after draining input for the pri expect(calls).toHaveLength(1); expect(calls[0]?.agent).toBe("codex"); - expect(calls[0]?.prompt).toContain( - '' - ); expect(calls[0]?.prompt).toContain("Claude: Please verify"); expect(calls[0]?.prompt).toContain( "Please verify the implementation details."