mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-11 01:01:13 +00:00
fix(agents): invalidate stale cli sessions on auth changes
This commit is contained in:
@@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- Agents/CLI backends: invalidate stored CLI session reuse when local CLI login state or the selected auth profile credential changes, so relogin and token rotation stop resuming stale sessions.
|
||||
- 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.
|
||||
|
||||
@@ -170,6 +170,7 @@ Serialization notes:
|
||||
- `serialize: true` keeps same-lane runs ordered.
|
||||
- Most CLIs serialize on one provider lane.
|
||||
- `claude-cli` is narrower: resumed runs serialize per Claude session id, and fresh runs serialize per workspace path. Independent workspaces can run in parallel.
|
||||
- OpenClaw drops stored CLI session reuse when the backend auth state changes, including relogin, token rotation, or a changed auth profile credential.
|
||||
|
||||
## Images (pass-through)
|
||||
|
||||
|
||||
144
src/agents/cli-auth-epoch.test.ts
Normal file
144
src/agents/cli-auth-epoch.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { AuthProfileStore } from "./auth-profiles/types.js";
|
||||
import {
|
||||
resetCliAuthEpochTestDeps,
|
||||
resolveCliAuthEpoch,
|
||||
setCliAuthEpochTestDeps,
|
||||
} from "./cli-auth-epoch.js";
|
||||
|
||||
describe("resolveCliAuthEpoch", () => {
|
||||
afterEach(() => {
|
||||
resetCliAuthEpochTestDeps();
|
||||
});
|
||||
|
||||
it("returns undefined when no local or auth-profile credentials exist", async () => {
|
||||
setCliAuthEpochTestDeps({
|
||||
readClaudeCliCredentialsCached: () => null,
|
||||
readCodexCliCredentialsCached: () => null,
|
||||
loadAuthProfileStoreForRuntime: () => ({
|
||||
version: 1,
|
||||
profiles: {},
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(resolveCliAuthEpoch({ provider: "claude-cli" })).resolves.toBeUndefined();
|
||||
await expect(
|
||||
resolveCliAuthEpoch({
|
||||
provider: "google-gemini-cli",
|
||||
authProfileId: "google:work",
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("changes when claude cli credentials change", async () => {
|
||||
let access = "access-a";
|
||||
setCliAuthEpochTestDeps({
|
||||
readClaudeCliCredentialsCached: () => ({
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access,
|
||||
refresh: "refresh",
|
||||
expires: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
const first = await resolveCliAuthEpoch({ provider: "claude-cli" });
|
||||
access = "access-b";
|
||||
const second = await resolveCliAuthEpoch({ provider: "claude-cli" });
|
||||
|
||||
expect(first).toBeDefined();
|
||||
expect(second).toBeDefined();
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("changes when auth profile credentials change", async () => {
|
||||
let store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:work": {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access-a",
|
||||
refresh: "refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
setCliAuthEpochTestDeps({
|
||||
loadAuthProfileStoreForRuntime: () => store,
|
||||
});
|
||||
|
||||
const first = await resolveCliAuthEpoch({
|
||||
provider: "google-gemini-cli",
|
||||
authProfileId: "anthropic:work",
|
||||
});
|
||||
store = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:work": {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access-b",
|
||||
refresh: "refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
const second = await resolveCliAuthEpoch({
|
||||
provider: "google-gemini-cli",
|
||||
authProfileId: "anthropic:work",
|
||||
});
|
||||
|
||||
expect(first).toBeDefined();
|
||||
expect(second).toBeDefined();
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("mixes local codex and auth-profile state", async () => {
|
||||
let access = "local-access-a";
|
||||
let refresh = "profile-refresh-a";
|
||||
setCliAuthEpochTestDeps({
|
||||
readCodexCliCredentialsCached: () => ({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access,
|
||||
refresh: "local-refresh",
|
||||
expires: 1,
|
||||
accountId: "acct-1",
|
||||
}),
|
||||
loadAuthProfileStoreForRuntime: () => ({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:work": {
|
||||
type: "oauth",
|
||||
provider: "openai",
|
||||
access: "profile-access",
|
||||
refresh,
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const first = await resolveCliAuthEpoch({
|
||||
provider: "codex-cli",
|
||||
authProfileId: "openai:work",
|
||||
});
|
||||
access = "local-access-b";
|
||||
const second = await resolveCliAuthEpoch({
|
||||
provider: "codex-cli",
|
||||
authProfileId: "openai:work",
|
||||
});
|
||||
refresh = "profile-refresh-b";
|
||||
const third = await resolveCliAuthEpoch({
|
||||
provider: "codex-cli",
|
||||
authProfileId: "openai:work",
|
||||
});
|
||||
|
||||
expect(first).toBeDefined();
|
||||
expect(second).toBeDefined();
|
||||
expect(third).toBeDefined();
|
||||
expect(second).not.toBe(first);
|
||||
expect(third).not.toBe(second);
|
||||
});
|
||||
});
|
||||
165
src/agents/cli-auth-epoch.ts
Normal file
165
src/agents/cli-auth-epoch.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import crypto from "node:crypto";
|
||||
import { loadAuthProfileStoreForRuntime } from "./auth-profiles/store.js";
|
||||
import type { AuthProfileCredential, AuthProfileStore } from "./auth-profiles/types.js";
|
||||
import {
|
||||
readClaudeCliCredentialsCached,
|
||||
readCodexCliCredentialsCached,
|
||||
type ClaudeCliCredential,
|
||||
type CodexCliCredential,
|
||||
} from "./cli-credentials.js";
|
||||
|
||||
type CliAuthEpochDeps = {
|
||||
readClaudeCliCredentialsCached: typeof readClaudeCliCredentialsCached;
|
||||
readCodexCliCredentialsCached: typeof readCodexCliCredentialsCached;
|
||||
loadAuthProfileStoreForRuntime: typeof loadAuthProfileStoreForRuntime;
|
||||
};
|
||||
|
||||
const defaultCliAuthEpochDeps: CliAuthEpochDeps = {
|
||||
readClaudeCliCredentialsCached,
|
||||
readCodexCliCredentialsCached,
|
||||
loadAuthProfileStoreForRuntime,
|
||||
};
|
||||
|
||||
const cliAuthEpochDeps: CliAuthEpochDeps = { ...defaultCliAuthEpochDeps };
|
||||
|
||||
export function setCliAuthEpochTestDeps(overrides: Partial<CliAuthEpochDeps>): void {
|
||||
Object.assign(cliAuthEpochDeps, overrides);
|
||||
}
|
||||
|
||||
export function resetCliAuthEpochTestDeps(): void {
|
||||
Object.assign(cliAuthEpochDeps, defaultCliAuthEpochDeps);
|
||||
}
|
||||
|
||||
function hashCliAuthEpochPart(value: string): string {
|
||||
return crypto.createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function encodeUnknown(value: unknown): string {
|
||||
return JSON.stringify(value ?? null);
|
||||
}
|
||||
|
||||
function encodeClaudeCredential(credential: ClaudeCliCredential): string {
|
||||
if (credential.type === "oauth") {
|
||||
return JSON.stringify([
|
||||
"oauth",
|
||||
credential.provider,
|
||||
credential.access,
|
||||
credential.refresh,
|
||||
credential.expires,
|
||||
]);
|
||||
}
|
||||
return JSON.stringify(["token", credential.provider, credential.token, credential.expires]);
|
||||
}
|
||||
|
||||
function encodeCodexCredential(credential: CodexCliCredential): string {
|
||||
return JSON.stringify([
|
||||
credential.type,
|
||||
credential.provider,
|
||||
credential.access,
|
||||
credential.refresh,
|
||||
credential.expires,
|
||||
credential.accountId ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
function encodeAuthProfileCredential(credential: AuthProfileCredential): string {
|
||||
switch (credential.type) {
|
||||
case "api_key":
|
||||
return JSON.stringify([
|
||||
"api_key",
|
||||
credential.provider,
|
||||
credential.key ?? null,
|
||||
encodeUnknown(credential.keyRef),
|
||||
credential.email ?? null,
|
||||
credential.displayName ?? null,
|
||||
encodeUnknown(credential.metadata),
|
||||
]);
|
||||
case "token":
|
||||
return JSON.stringify([
|
||||
"token",
|
||||
credential.provider,
|
||||
credential.token ?? null,
|
||||
encodeUnknown(credential.tokenRef),
|
||||
credential.expires ?? null,
|
||||
credential.email ?? null,
|
||||
credential.displayName ?? null,
|
||||
]);
|
||||
case "oauth":
|
||||
return JSON.stringify([
|
||||
"oauth",
|
||||
credential.provider,
|
||||
credential.access,
|
||||
credential.refresh,
|
||||
credential.expires,
|
||||
credential.clientId ?? null,
|
||||
credential.email ?? null,
|
||||
credential.displayName ?? null,
|
||||
credential.enterpriseUrl ?? null,
|
||||
credential.projectId ?? null,
|
||||
credential.accountId ?? null,
|
||||
credential.managedBy ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
function getLocalCliCredentialFingerprint(provider: string): string | undefined {
|
||||
switch (provider) {
|
||||
case "claude-cli": {
|
||||
const credential = cliAuthEpochDeps.readClaudeCliCredentialsCached({
|
||||
ttlMs: 5000,
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
return credential ? hashCliAuthEpochPart(encodeClaudeCredential(credential)) : undefined;
|
||||
}
|
||||
case "codex-cli": {
|
||||
const credential = cliAuthEpochDeps.readCodexCliCredentialsCached({
|
||||
ttlMs: 5000,
|
||||
});
|
||||
return credential ? hashCliAuthEpochPart(encodeCodexCredential(credential)) : undefined;
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthProfileCredential(
|
||||
store: AuthProfileStore,
|
||||
authProfileId: string | undefined,
|
||||
): AuthProfileCredential | undefined {
|
||||
if (!authProfileId) {
|
||||
return undefined;
|
||||
}
|
||||
return store.profiles[authProfileId];
|
||||
}
|
||||
|
||||
export async function resolveCliAuthEpoch(params: {
|
||||
provider: string;
|
||||
authProfileId?: string;
|
||||
}): Promise<string | undefined> {
|
||||
const provider = params.provider.trim();
|
||||
const authProfileId = params.authProfileId?.trim() || undefined;
|
||||
const parts: string[] = [];
|
||||
|
||||
const localFingerprint = getLocalCliCredentialFingerprint(provider);
|
||||
if (localFingerprint) {
|
||||
parts.push(`local:${provider}:${localFingerprint}`);
|
||||
}
|
||||
|
||||
if (authProfileId) {
|
||||
const store = cliAuthEpochDeps.loadAuthProfileStoreForRuntime(undefined, {
|
||||
readOnly: true,
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const credential = getAuthProfileCredential(store, authProfileId);
|
||||
if (credential) {
|
||||
parts.push(
|
||||
`profile:${authProfileId}:${hashCliAuthEpochPart(encodeAuthProfileCredential(credential))}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return hashCliAuthEpochPart(parts.join("\n"));
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export async function runCliAgent(params: RunCliAgentParams): Promise<EmbeddedPi
|
||||
cliSessionBinding: {
|
||||
sessionId: resultParams.effectiveCliSessionId,
|
||||
...(params.authProfileId ? { authProfileId: params.authProfileId } : {}),
|
||||
...(context.authEpoch ? { authEpoch: context.authEpoch } : {}),
|
||||
...(context.extraSystemPromptHash
|
||||
? { extraSystemPromptHash: context.extraSystemPromptHash }
|
||||
: {}),
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
makeBootstrapWarn as makeBootstrapWarnImpl,
|
||||
resolveBootstrapContextForRun as resolveBootstrapContextForRunImpl,
|
||||
} from "../bootstrap-files.js";
|
||||
import { resolveCliAuthEpoch } from "../cli-auth-epoch.js";
|
||||
import { resolveCliBackendConfig } from "../cli-backends.js";
|
||||
import { hashCliSessionText, resolveCliSessionReuse } from "../cli-session.js";
|
||||
import { resolveOpenClawDocsPath } from "../docs-path.js";
|
||||
@@ -65,6 +66,10 @@ export async function prepareCliRunContext(
|
||||
if (!backendResolved) {
|
||||
throw new Error(`Unknown CLI backend: ${params.provider}`);
|
||||
}
|
||||
const authEpoch = await resolveCliAuthEpoch({
|
||||
provider: params.provider,
|
||||
authProfileId: params.authProfileId,
|
||||
});
|
||||
const extraSystemPrompt = params.extraSystemPrompt?.trim() ?? "";
|
||||
const extraSystemPromptHash = hashCliSessionText(extraSystemPrompt);
|
||||
const modelId = (params.model ?? "default").trim() || "default";
|
||||
@@ -130,6 +135,7 @@ export async function prepareCliRunContext(
|
||||
params.cliSessionBinding ??
|
||||
(params.cliSessionId ? { sessionId: params.cliSessionId } : undefined),
|
||||
authProfileId: params.authProfileId,
|
||||
authEpoch,
|
||||
extraSystemPromptHash,
|
||||
mcpConfigHash: preparedBackend.mcpConfigHash,
|
||||
});
|
||||
@@ -197,6 +203,7 @@ export async function prepareCliRunContext(
|
||||
systemPromptReport,
|
||||
bootstrapPromptWarningLines: bootstrapPromptWarning.lines,
|
||||
heartbeatPrompt,
|
||||
authEpoch,
|
||||
extraSystemPromptHash,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export type CliPreparedBackend = {
|
||||
|
||||
export type CliReusableSession = {
|
||||
sessionId?: string;
|
||||
invalidatedReason?: "auth-profile" | "system-prompt" | "mcp";
|
||||
invalidatedReason?: "auth-profile" | "auth-epoch" | "system-prompt" | "mcp";
|
||||
};
|
||||
|
||||
export type PreparedCliRunContext = {
|
||||
@@ -59,5 +59,6 @@ export type PreparedCliRunContext = {
|
||||
systemPromptReport: SessionSystemPromptReport;
|
||||
bootstrapPromptWarningLines: string[];
|
||||
heartbeatPrompt?: string;
|
||||
authEpoch?: string;
|
||||
extraSystemPromptHash?: string;
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("cli-session helpers", () => {
|
||||
setCliSessionBinding(entry, "claude-cli", {
|
||||
sessionId: "cli-session-1",
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "auth-epoch",
|
||||
extraSystemPromptHash: "prompt-hash",
|
||||
mcpConfigHash: "mcp-hash",
|
||||
});
|
||||
@@ -28,6 +29,7 @@ describe("cli-session helpers", () => {
|
||||
expect(getCliSessionBinding(entry, "claude-cli")).toEqual({
|
||||
sessionId: "cli-session-1",
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "auth-epoch",
|
||||
extraSystemPromptHash: "prompt-hash",
|
||||
mcpConfigHash: "mcp-hash",
|
||||
});
|
||||
@@ -79,6 +81,7 @@ describe("cli-session helpers", () => {
|
||||
const binding = {
|
||||
sessionId: "cli-session-1",
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "auth-epoch-a",
|
||||
extraSystemPromptHash: "prompt-a",
|
||||
mcpConfigHash: "mcp-a",
|
||||
};
|
||||
@@ -87,6 +90,7 @@ describe("cli-session helpers", () => {
|
||||
resolveCliSessionReuse({
|
||||
binding,
|
||||
authProfileId: "anthropic:personal",
|
||||
authEpoch: "auth-epoch-a",
|
||||
extraSystemPromptHash: "prompt-a",
|
||||
mcpConfigHash: "mcp-a",
|
||||
}),
|
||||
@@ -95,6 +99,16 @@ describe("cli-session helpers", () => {
|
||||
resolveCliSessionReuse({
|
||||
binding,
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "auth-epoch-b",
|
||||
extraSystemPromptHash: "prompt-a",
|
||||
mcpConfigHash: "mcp-a",
|
||||
}),
|
||||
).toEqual({ invalidatedReason: "auth-epoch" });
|
||||
expect(
|
||||
resolveCliSessionReuse({
|
||||
binding,
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "auth-epoch-a",
|
||||
extraSystemPromptHash: "prompt-b",
|
||||
mcpConfigHash: "mcp-a",
|
||||
}),
|
||||
@@ -103,6 +117,7 @@ describe("cli-session helpers", () => {
|
||||
resolveCliSessionReuse({
|
||||
binding,
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "auth-epoch-a",
|
||||
extraSystemPromptHash: "prompt-a",
|
||||
mcpConfigHash: "mcp-b",
|
||||
}),
|
||||
@@ -113,6 +128,7 @@ describe("cli-session helpers", () => {
|
||||
const binding = {
|
||||
sessionId: "cli-session-1",
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "auth-epoch-a",
|
||||
extraSystemPromptHash: "prompt-a",
|
||||
mcpConfigHash: "mcp-a",
|
||||
};
|
||||
@@ -121,6 +137,7 @@ describe("cli-session helpers", () => {
|
||||
resolveCliSessionReuse({
|
||||
binding,
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "auth-epoch-a",
|
||||
extraSystemPromptHash: "prompt-a",
|
||||
mcpConfigHash: "mcp-a",
|
||||
}),
|
||||
|
||||
@@ -31,6 +31,7 @@ export function getCliSessionBinding(
|
||||
return {
|
||||
sessionId: bindingSessionId,
|
||||
authProfileId: trimOptional(fromBindings?.authProfileId),
|
||||
authEpoch: trimOptional(fromBindings?.authEpoch),
|
||||
extraSystemPromptHash: trimOptional(fromBindings?.extraSystemPromptHash),
|
||||
mcpConfigHash: trimOptional(fromBindings?.mcpConfigHash),
|
||||
};
|
||||
@@ -76,6 +77,7 @@ export function setCliSessionBinding(
|
||||
...(trimOptional(binding.authProfileId)
|
||||
? { authProfileId: trimOptional(binding.authProfileId) }
|
||||
: {}),
|
||||
...(trimOptional(binding.authEpoch) ? { authEpoch: trimOptional(binding.authEpoch) } : {}),
|
||||
...(trimOptional(binding.extraSystemPromptHash)
|
||||
? { extraSystemPromptHash: trimOptional(binding.extraSystemPromptHash) }
|
||||
: {}),
|
||||
@@ -116,21 +118,30 @@ export function clearAllCliSessions(entry: SessionEntry): void {
|
||||
export function resolveCliSessionReuse(params: {
|
||||
binding?: CliSessionBinding;
|
||||
authProfileId?: string;
|
||||
authEpoch?: string;
|
||||
extraSystemPromptHash?: string;
|
||||
mcpConfigHash?: string;
|
||||
}): { sessionId?: string; invalidatedReason?: "auth-profile" | "system-prompt" | "mcp" } {
|
||||
}): {
|
||||
sessionId?: string;
|
||||
invalidatedReason?: "auth-profile" | "auth-epoch" | "system-prompt" | "mcp";
|
||||
} {
|
||||
const binding = params.binding;
|
||||
const sessionId = trimOptional(binding?.sessionId);
|
||||
if (!sessionId) {
|
||||
return {};
|
||||
}
|
||||
const currentAuthProfileId = trimOptional(params.authProfileId);
|
||||
const currentAuthEpoch = trimOptional(params.authEpoch);
|
||||
const currentExtraSystemPromptHash = trimOptional(params.extraSystemPromptHash);
|
||||
const currentMcpConfigHash = trimOptional(params.mcpConfigHash);
|
||||
const storedAuthProfileId = trimOptional(binding?.authProfileId);
|
||||
if (storedAuthProfileId !== currentAuthProfileId) {
|
||||
return { invalidatedReason: "auth-profile" };
|
||||
}
|
||||
const storedAuthEpoch = trimOptional(binding?.authEpoch);
|
||||
if (storedAuthEpoch !== currentAuthEpoch) {
|
||||
return { invalidatedReason: "auth-epoch" };
|
||||
}
|
||||
const storedExtraSystemPromptHash = trimOptional(binding?.extraSystemPromptHash);
|
||||
if (storedExtraSystemPromptHash !== currentExtraSystemPromptHash) {
|
||||
return { invalidatedReason: "system-prompt" };
|
||||
|
||||
@@ -167,6 +167,7 @@ describe("updateSessionStoreAfterAgentRun", () => {
|
||||
sessionId: "claude-cli-session-1",
|
||||
cliSessionBinding: {
|
||||
sessionId: "claude-cli-session-1",
|
||||
authEpoch: "auth-epoch-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -181,11 +182,13 @@ describe("updateSessionStoreAfterAgentRun", () => {
|
||||
expect(second.sessionKey).toBe(first.sessionKey);
|
||||
expect(second.sessionEntry?.cliSessionBindings?.["claude-cli"]).toEqual({
|
||||
sessionId: "claude-cli-session-1",
|
||||
authEpoch: "auth-epoch-1",
|
||||
});
|
||||
|
||||
const persisted = loadSessionStore(storePath, { skipCache: true })[first.sessionKey!];
|
||||
expect(persisted?.cliSessionBindings?.["claude-cli"]).toEqual({
|
||||
sessionId: "claude-cli-session-1",
|
||||
authEpoch: "auth-epoch-1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,6 +68,7 @@ export type AcpSessionRuntimeOptions = {
|
||||
export type CliSessionBinding = {
|
||||
sessionId: string;
|
||||
authProfileId?: string;
|
||||
authEpoch?: string;
|
||||
extraSystemPromptHash?: string;
|
||||
mcpConfigHash?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user