mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 20:56:54 +00:00
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:
committed by
GitHub
parent
c2eb12bbc5
commit
aca216bfcf
@@ -234,6 +234,7 @@ export class AcpSessionManager {
|
||||
sessionKey,
|
||||
agent,
|
||||
mode: input.mode,
|
||||
resumeSessionId: input.resumeSessionId,
|
||||
cwd: requestedCwd,
|
||||
}),
|
||||
fallbackCode: "ACP_SESSION_INIT_FAILED",
|
||||
|
||||
@@ -43,6 +43,7 @@ export type AcpInitializeSessionInput = {
|
||||
sessionKey: string;
|
||||
agent: string;
|
||||
mode: AcpRuntimeSessionMode;
|
||||
resumeSessionId?: string;
|
||||
cwd?: string;
|
||||
backendId?: string;
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ export type AcpRuntimeEnsureInput = {
|
||||
sessionKey: string;
|
||||
agent: string;
|
||||
mode: AcpRuntimeSessionMode;
|
||||
resumeSessionId?: string;
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user