feat(acp): add resumeSessionId to sessions_spawn for ACP session resume (#41847)

* feat(acp): add resumeSessionId to sessions_spawn for ACP session resume

Thread resumeSessionId through the ACP session spawn pipeline so agents
can resume existing sessions (e.g. a prior Codex conversation) instead
of starting fresh.

Flow: sessions_spawn tool → spawnAcpDirect → initializeSession →
ensureSession → acpx --resume-session flag → agent session/load

- Add resumeSessionId param to sessions-spawn-tool schema with
  description so agents can discover and use it
- Thread through SpawnAcpParams → AcpInitializeSessionInput →
  AcpRuntimeEnsureInput → acpx extension runtime
- Pass as --resume-session flag to acpx CLI
- Error hard (exit 4) on non-existent session, no silent fallback
- All new fields optional for backward compatibility

Depends on acpx >= 0.1.16 (openclaw/acpx#85, merged, pending release).

Tests: 26/26 pass (runtime + tool schema)
Verified e2e: Discord → sessions_spawn(resumeSessionId) → Codex
resumed session and recalled stored secret.

🤖 AI-assisted

* fix: guard resumeSessionId against non-ACP runtime

Add early-return error when resumeSessionId is passed without
runtime="acp" (mirrors existing streamTo guard). Without this,
the parameter is silently ignored and the agent gets a fresh
session instead of resuming.

Also update schema description to note the runtime=acp requirement.

Addresses Greptile review feedback.

* ACP: add changelog entry for session resume (#41847) (thanks @pejmanjohn)

---------

Co-authored-by: Pejman Pour-Moezzi <481729+pejmanjohn@users.noreply.github.com>
Co-authored-by: Onur <onur@textcortex.com>
This commit is contained in:
Pejman Pour-Moezzi
2026-03-10 02:36:13 -07:00
committed by GitHub
parent c2eb12bbc5
commit aca216bfcf
9 changed files with 98 additions and 8 deletions

View File

@@ -234,6 +234,7 @@ export class AcpSessionManager {
sessionKey,
agent,
mode: input.mode,
resumeSessionId: input.resumeSessionId,
cwd: requestedCwd,
}),
fallbackCode: "ACP_SESSION_INIT_FAILED",

View File

@@ -43,6 +43,7 @@ export type AcpInitializeSessionInput = {
sessionKey: string;
agent: string;
mode: AcpRuntimeSessionMode;
resumeSessionId?: string;
cwd?: string;
backendId?: string;
};

View File

@@ -35,6 +35,7 @@ export type AcpRuntimeEnsureInput = {
sessionKey: string;
agent: string;
mode: AcpRuntimeSessionMode;
resumeSessionId?: string;
cwd?: string;
env?: Record<string, string>;
};

View File

@@ -56,6 +56,7 @@ export type SpawnAcpParams = {
task: string;
label?: string;
agentId?: string;
resumeSessionId?: string;
cwd?: string;
mode?: SpawnAcpMode;
thread?: boolean;
@@ -426,6 +427,7 @@ export async function spawnAcpDirect(
sessionKey,
agent: targetAgentId,
mode: runtimeMode,
resumeSessionId: params.resumeSessionId,
cwd: params.cwd,
backendId: cfg.acp?.backend,
});

View File

@@ -163,6 +163,43 @@ describe("sessions_spawn tool", () => {
);
});
it("passes resumeSessionId through to ACP spawns", async () => {
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
});
await tool.execute("call-2c", {
runtime: "acp",
task: "resume prior work",
agentId: "codex",
resumeSessionId: "7f4a78e0-f6be-43fe-855c-c1c4fd229bc4",
});
expect(hoisted.spawnAcpDirectMock).toHaveBeenCalledWith(
expect.objectContaining({
task: "resume prior work",
agentId: "codex",
resumeSessionId: "7f4a78e0-f6be-43fe-855c-c1c4fd229bc4",
}),
expect.any(Object),
);
});
it("rejects resumeSessionId without runtime=acp", async () => {
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
});
const result = await tool.execute("call-guard", {
task: "resume prior work",
resumeSessionId: "7f4a78e0-f6be-43fe-855c-c1c4fd229bc4",
});
expect(JSON.stringify(result)).toContain("resumeSessionId is only supported for runtime=acp");
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
});
it("rejects attachments for ACP runtime", async () => {
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",

View File

@@ -25,6 +25,12 @@ const SessionsSpawnToolSchema = Type.Object({
label: Type.Optional(Type.String()),
runtime: optionalStringEnum(SESSIONS_SPAWN_RUNTIMES),
agentId: Type.Optional(Type.String()),
resumeSessionId: Type.Optional(
Type.String({
description:
'Resume an existing agent session by its ID (e.g. a Codex session UUID from ~/.codex/sessions/). Requires runtime="acp". The agent replays conversation history via session/load instead of starting fresh.',
}),
),
model: Type.Optional(Type.String()),
thinking: Type.Optional(Type.String()),
cwd: Type.Optional(Type.String()),
@@ -91,6 +97,7 @@ export function createSessionsSpawnTool(
const label = typeof params.label === "string" ? params.label.trim() : "";
const runtime = params.runtime === "acp" ? "acp" : "subagent";
const requestedAgentId = readStringParam(params, "agentId");
const resumeSessionId = readStringParam(params, "resumeSessionId");
const modelOverride = readStringParam(params, "model");
const thinkingOverrideRaw = readStringParam(params, "thinking");
const cwd = readStringParam(params, "cwd");
@@ -127,6 +134,13 @@ export function createSessionsSpawnTool(
});
}
if (resumeSessionId && runtime !== "acp") {
return jsonResult({
status: "error",
error: `resumeSessionId is only supported for runtime=acp; got runtime=${runtime}`,
});
}
if (runtime === "acp") {
if (Array.isArray(attachments) && attachments.length > 0) {
return jsonResult({
@@ -140,6 +154,7 @@ export function createSessionsSpawnTool(
task,
label: label || undefined,
agentId: requestedAgentId,
resumeSessionId,
cwd,
mode: mode && ACP_SPAWN_MODES.includes(mode) ? mode : undefined,
thread,