mirror of https://github.com/openclaw/openclaw.git
fix: resume explicit session-id agent runs
This commit is contained in:
parent
87f512f80d
commit
cd36ff7483
|
|
@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
|
|||
- WhatsApp: restore `channels.whatsapp.blockStreaming` and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069)
|
||||
- Control UI: keep Stop visible during tool-only execution, preserve pending-send busy state, and clear stale ClawHub search results as soon as the query changes. (#54528, #59800, #60267)
|
||||
- MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198)
|
||||
- Agents/Claude CLI: persist explicit `openclaw agent --session-id` runs under a stable session key so follow-ups can reuse the stored CLI binding and resume the same underlying Claude session.
|
||||
- Auth/failover: persist selected fallback overrides before retrying, shorten `auth_permanent` lockouts, and refresh websocket/shared-auth sessions only when real auth changes occur so retries and secret rotations behave predictably. (#60404, #60323, #60387)
|
||||
- Cron: replay interrupted recurring jobs on the first gateway restart instead of waiting for a second restart. (#60583) Thanks @joelnishanth.
|
||||
- Plugins/media understanding: enable bundled Groq and Deepgram providers by default so configured transcription models work without extra plugin activation config. (#59982) Thanks @yxjsxy.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "../../config/sessions.js";
|
||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { normalizeAgentId, normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { resolveSessionIdMatchSelection } from "../../sessions/session-id-resolution.js";
|
||||
import { listAgentIds } from "../agent-scope.js";
|
||||
import { clearBootstrapSnapshotOnSessionRollover } from "../bootstrap-cache.js";
|
||||
|
|
@ -47,6 +47,10 @@ type SessionIdMatchSet = {
|
|||
storeByKey: Map<string, SessionKeyResolution>;
|
||||
};
|
||||
|
||||
function buildExplicitSessionIdSessionKey(params: { sessionId: string; agentId?: string }): string {
|
||||
return `agent:${normalizeAgentId(params.agentId)}:explicit:${params.sessionId.trim()}`;
|
||||
}
|
||||
|
||||
function collectSessionIdMatchesForRequest(opts: {
|
||||
cfg: OpenClawConfig;
|
||||
sessionStore: Record<string, SessionEntry>;
|
||||
|
|
@ -146,6 +150,13 @@ export function resolveSessionKeyForRequest(opts: {
|
|||
}
|
||||
}
|
||||
|
||||
if (opts.sessionId && !sessionKey) {
|
||||
sessionKey = buildExplicitSessionIdSessionKey({
|
||||
sessionId: opts.sessionId,
|
||||
agentId: opts.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
return { sessionKey, sessionStore, storePath };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { __testing as acpManagerTesting } from "../acp/control-plane/manager.js"
|
|||
import { resolveAgentDir, resolveSessionAgentId } from "../agents/agent-scope.js";
|
||||
import * as authProfilesModule from "../agents/auth-profiles.js";
|
||||
import * as cliRunnerModule from "../agents/cli-runner.js";
|
||||
import * as sessionStoreModule from "../agents/command/session-store.js";
|
||||
import { resolveSession } from "../agents/command/session.js";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import * as modelSelectionModule from "../agents/model-selection.js";
|
||||
|
|
@ -555,6 +556,52 @@ describe("agentCommand", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("creates a stable session key for explicit session-id-only runs", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
const cfg = mockConfig(home, store);
|
||||
|
||||
const resolution = resolveSession({ cfg, sessionId: "explicit-session-123" });
|
||||
|
||||
expect(resolution.sessionKey).toBe("agent:main:explicit:explicit-session-123");
|
||||
expect(resolution.sessionId).toBe("explicit-session-123");
|
||||
});
|
||||
});
|
||||
|
||||
it("persists explicit session-id-only CLI runs with the synthetic session key", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const store = path.join(home, "sessions.json");
|
||||
mockConfig(home, store, {
|
||||
model: { primary: "claude-cli/claude-sonnet-4-6" },
|
||||
models: { "claude-cli/claude-sonnet-4-6": {} },
|
||||
});
|
||||
vi.mocked(modelSelectionModule.isCliProvider).mockImplementation(() => true);
|
||||
runCliAgentSpy.mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: {
|
||||
durationMs: 5,
|
||||
agentMeta: {
|
||||
sessionId: "claude-cli-session-1",
|
||||
provider: "claude-cli",
|
||||
model: "claude-sonnet-4-6",
|
||||
cliSessionBinding: {
|
||||
sessionId: "claude-cli-session-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
await agentCommand({ message: "resume me", sessionId: "explicit-session-123" }, runtime);
|
||||
|
||||
expect(vi.mocked(sessionStoreModule.updateSessionStoreAfterAgentRun)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionId: "explicit-session-123",
|
||||
sessionKey: "agent:main:explicit:explicit-session-123",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the resumed session agent scope when sessionId resolves to another agent store", async () => {
|
||||
await withCrossAgentResumeFixture(async ({ sessionId, sessionKey, cfg }) => {
|
||||
const resolution = resolveSession({ cfg, sessionId });
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSession } from "../../agents/command/session.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { loadSessionStore } from "../../config/sessions.js";
|
||||
import { updateSessionStoreAfterAgentRun } from "./session-store.js";
|
||||
|
|
@ -124,4 +125,67 @@ describe("updateSessionStoreAfterAgentRun", () => {
|
|||
"once",
|
||||
);
|
||||
});
|
||||
|
||||
it("stores and reloads CLI bindings for explicit session-id-only runs", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-store-"));
|
||||
const storePath = path.join(dir, "sessions.json");
|
||||
const cfg = {
|
||||
session: {
|
||||
store: storePath,
|
||||
mainKey: "main",
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
cliBackends: {
|
||||
"claude-cli": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never;
|
||||
|
||||
const first = resolveSession({
|
||||
cfg,
|
||||
sessionId: "explicit-session-123",
|
||||
});
|
||||
|
||||
expect(first.sessionKey).toBe("agent:main:explicit:explicit-session-123");
|
||||
|
||||
await updateSessionStoreAfterAgentRun({
|
||||
cfg,
|
||||
sessionId: first.sessionId,
|
||||
sessionKey: first.sessionKey!,
|
||||
storePath: first.storePath,
|
||||
sessionStore: first.sessionStore!,
|
||||
defaultProvider: "claude-cli",
|
||||
defaultModel: "claude-sonnet-4-6",
|
||||
result: {
|
||||
payloads: [],
|
||||
meta: {
|
||||
agentMeta: {
|
||||
provider: "claude-cli",
|
||||
model: "claude-sonnet-4-6",
|
||||
sessionId: "claude-cli-session-1",
|
||||
cliSessionBinding: {
|
||||
sessionId: "claude-cli-session-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
const second = resolveSession({
|
||||
cfg,
|
||||
sessionId: "explicit-session-123",
|
||||
});
|
||||
|
||||
expect(second.sessionKey).toBe(first.sessionKey);
|
||||
expect(second.sessionEntry?.cliSessionBindings?.["claude-cli"]).toEqual({
|
||||
sessionId: "claude-cli-session-1",
|
||||
});
|
||||
|
||||
const persisted = loadSessionStore(storePath, { skipCache: true })[first.sessionKey!];
|
||||
expect(persisted?.cliSessionBindings?.["claude-cli"]).toEqual({
|
||||
sessionId: "claude-cli-session-1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue