Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
}
},
"202": {
"description": "JSON-RPC notification accepted"
"description": "JSON-RPC notification accepted, or long-running request accepted with its response delivered over SSE"
},
"400": {
"description": "Invalid ACP envelope",
Expand Down
10 changes: 10 additions & 0 deletions research/acp/friction.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,13 @@ Update this file continuously during the migration.
- Owner: Unassigned.
- Status: resolved
- Links: `server/packages/sandbox-agent/src/router.rs`, `server/packages/sandbox-agent/src/desktop_runtime.rs`, `sdks/typescript/src/client.ts`, `frontend/packages/inspector/src/components/debug/DesktopTab.tsx`

- Date: 2026-07-20
- Area: Long-running ACP requests over streamable HTTP
- Issue: `session/prompt` kept its POST open until the agent completed the turn, making successful execution depend on client and proxy response-header timeouts despite an existing SSE response channel.
- Impact: Long turns could lose their POST connection after several minutes, terminate request handling, or require downstream clients to configure unusually long HTTP timeouts.
- Proposed direction: Return `202 Accepted` after `session/prompt` is written to the agent, then deliver its correlated JSON-RPC result or error through the existing SSE stream. Keep short requests synchronous and preserve immediate HTTP errors before acceptance.
- Decision: Proposed and implemented for review.
- Owner: Unassigned.
- Status: in_progress
- Links: `server/packages/acp-http-adapter/src/process.rs`, `server/packages/sandbox-agent/tests/v1_api/acp_transport.rs`, https://github.com/rivet-dev/sandbox-agent/issues/305
6 changes: 3 additions & 3 deletions sdks/acp-http-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,9 @@ class StreamableHttpAcpTransport {
return;
}

// SSE failure is non-fatal: the POST request/response flow still works.
// Exiting the loop allows ensureSseLoop() to restart it on the next POST.
return;
// Prompt responses can be delivered exclusively over SSE after a 202.
// Reconnect without waiting for another POST, replaying from Last-Event-ID.
await delay(150);
}
}
}
Expand Down
37 changes: 37 additions & 0 deletions sdks/acp-http-client/tests/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,41 @@ describe("AcpHttpClient integration", () => {

await client.disconnect();
});

it("reconnects SSE without another POST while a prompt is in flight", async () => {
const serverId = `acp-http-client-reconnect-${Date.now().toString(36)}`;
let remainingSseFailures = 3;
const reconnectingFetch: typeof fetch = async (input, init) => {
if (init?.method === "GET" && remainingSseFailures > 0) {
remainingSseFailures -= 1;
throw new TypeError("simulated SSE connection failure");
}
return globalThis.fetch(input, init);
};

const client = new AcpHttpClient({
baseUrl,
token,
fetch: reconnectingFetch,
transport: {
path: `/v1/acp/${encodeURIComponent(serverId)}`,
bootstrapQuery: { agent: "mock" },
},
});

await client.initialize();
const session = await client.newSession({
cwd: process.cwd(),
mcpServers: [],
});
const prompt = await client.prompt({
sessionId: session.sessionId,
prompt: [{ type: "text", text: "reconnect the event stream" }],
});

expect(remainingSseFailures).toBe(0);
expect(prompt.stopReason).toBe("end_turn");

await client.disconnect();
});
});
11 changes: 10 additions & 1 deletion sdks/typescript/src/generated/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,15 @@ export interface components {
directory: string;
skillName: string;
};
McpCommand: string | string[];
/** @enum {string} */
McpRemoteTransport: "http" | "sse";
McpOAuthConfig: {
clientId?: string | null;
clientSecret?: string | null;
scope?: string | null;
};
McpOAuthConfigOrDisabled: components["schemas"]["McpOAuthConfig"] | boolean;
};
responses: never;
parameters: never;
Expand Down Expand Up @@ -1083,7 +1092,7 @@ export interface operations {
"application/json": components["schemas"]["AcpEnvelope"];
};
};
/** @description JSON-RPC notification accepted */
/** @description JSON-RPC notification accepted, or long-running request accepted with its response delivered over SSE */
202: {
content: never;
};
Expand Down
5 changes: 2 additions & 3 deletions server/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,10 @@ Each session tracks:
POST /v1/acp/{serverId}?agent=... initialize ACP server, auto-install agent
POST /v1/acp/{serverId} session/new
POST /v1/acp/{serverId} session/prompt
GET /v1/acp/{serverId} Subscribe to ACP SSE stream
POST /v1/acp/{serverId} session/prompt → 202 Accepted
JSON-RPC response envelopes Answer questions / reply to permissions
JSON-RPC response over SSE Answer questions / reply to permissions
DELETE /v1/acp/{serverId} Close ACP server
```
Expand Down
Loading