fix: relay ACP sessions_spawn parent streaming (#34310) (thanks @vincentkoc) (#34310)

Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
This commit is contained in:
Bob
2026-03-04 11:44:20 +01:00
committed by GitHub
parent 61f7cea48b
commit 257e2f5338
10 changed files with 893 additions and 3 deletions

View File

@@ -32,12 +32,19 @@ import {
} from "../infra/outbound/session-binding-service.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { normalizeDeliveryContext } from "../utils/delivery-context.js";
import {
type AcpSpawnParentRelayHandle,
resolveAcpSpawnStreamLogPath,
startAcpSpawnParentStreamRelay,
} from "./acp-spawn-parent-stream.js";
import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
export const ACP_SPAWN_MODES = ["run", "session"] as const;
export type SpawnAcpMode = (typeof ACP_SPAWN_MODES)[number];
export const ACP_SPAWN_SANDBOX_MODES = ["inherit", "require"] as const;
export type SpawnAcpSandboxMode = (typeof ACP_SPAWN_SANDBOX_MODES)[number];
export const ACP_SPAWN_STREAM_TARGETS = ["parent"] as const;
export type SpawnAcpStreamTarget = (typeof ACP_SPAWN_STREAM_TARGETS)[number];
export type SpawnAcpParams = {
task: string;
@@ -47,6 +54,7 @@ export type SpawnAcpParams = {
mode?: SpawnAcpMode;
thread?: boolean;
sandbox?: SpawnAcpSandboxMode;
streamTo?: SpawnAcpStreamTarget;
};
export type SpawnAcpContext = {
@@ -63,6 +71,7 @@ export type SpawnAcpResult = {
childSessionKey?: string;
runId?: string;
mode?: SpawnAcpMode;
streamLogPath?: string;
note?: string;
error?: string;
};
@@ -234,6 +243,14 @@ export async function spawnAcpDirect(
};
}
const sandboxMode = params.sandbox === "require" ? "require" : "inherit";
const streamToParentRequested = params.streamTo === "parent";
const parentSessionKey = ctx.agentSessionKey?.trim();
if (streamToParentRequested && !parentSessionKey) {
return {
status: "error",
error: 'sessions_spawn streamTo="parent" requires an active requester session context.',
};
}
const requesterRuntime = resolveSandboxRuntimeStatus({
cfg,
sessionKey: ctx.agentSessionKey,
@@ -410,8 +427,27 @@ export async function spawnAcpDirect(
? `channel:${boundThreadId}`
: requesterOrigin?.to?.trim() || (deliveryThreadId ? `channel:${deliveryThreadId}` : undefined);
const hasDeliveryTarget = Boolean(requesterOrigin?.channel && inferredDeliveryTo);
const deliverToBoundTarget = hasDeliveryTarget && !streamToParentRequested;
const childIdem = crypto.randomUUID();
let childRunId: string = childIdem;
const streamLogPath =
streamToParentRequested && parentSessionKey
? resolveAcpSpawnStreamLogPath({
childSessionKey: sessionKey,
})
: undefined;
let parentRelay: AcpSpawnParentRelayHandle | undefined;
if (streamToParentRequested && parentSessionKey) {
// Register relay before dispatch so fast lifecycle failures are not missed.
parentRelay = startAcpSpawnParentStreamRelay({
runId: childIdem,
parentSessionKey,
childSessionKey: sessionKey,
agentId: targetAgentId,
logPath: streamLogPath,
emitStartNotice: false,
});
}
try {
const response = await callGateway<{ runId?: string }>({
method: "agent",
@@ -423,7 +459,7 @@ export async function spawnAcpDirect(
accountId: hasDeliveryTarget ? (requesterOrigin?.accountId ?? undefined) : undefined,
threadId: hasDeliveryTarget ? deliveryThreadId : undefined,
idempotencyKey: childIdem,
deliver: hasDeliveryTarget,
deliver: deliverToBoundTarget,
label: params.label || undefined,
},
timeoutMs: 10_000,
@@ -432,6 +468,7 @@ export async function spawnAcpDirect(
childRunId = response.runId.trim();
}
} catch (err) {
parentRelay?.dispose();
await cleanupFailedAcpSpawn({
cfg,
sessionKey,
@@ -445,6 +482,30 @@ export async function spawnAcpDirect(
};
}
if (streamToParentRequested && parentSessionKey) {
if (parentRelay && childRunId !== childIdem) {
parentRelay.dispose();
// Defensive fallback if gateway returns a runId that differs from idempotency key.
parentRelay = startAcpSpawnParentStreamRelay({
runId: childRunId,
parentSessionKey,
childSessionKey: sessionKey,
agentId: targetAgentId,
logPath: streamLogPath,
emitStartNotice: false,
});
}
parentRelay?.notifyStarted();
return {
status: "accepted",
childSessionKey: sessionKey,
runId: childRunId,
mode: spawnMode,
...(streamLogPath ? { streamLogPath } : {}),
note: spawnMode === "session" ? ACP_SPAWN_SESSION_ACCEPTED_NOTE : ACP_SPAWN_ACCEPTED_NOTE,
};
}
return {
status: "accepted",
childSessionKey: sessionKey,