diff --git a/packages/adapters/claude-local/src/server/execute.remote.test.ts b/packages/adapters/claude-local/src/server/execute.remote.test.ts index ddc3959a..7bc31ff0 100644 --- a/packages/adapters/claude-local/src/server/execute.remote.test.ts +++ b/packages/adapters/claude-local/src/server/execute.remote.test.ts @@ -229,12 +229,12 @@ describe("claude remote execution", () => { adapterConfig: {}, }, runtime: { - sessionId: "session-123", + sessionId: "12345678-1234-4abc-9def-123456789012", sessionParams: { - sessionId: "session-123", + sessionId: "12345678-1234-4abc-9def-123456789012", cwd: "/remote/workspace", }, - sessionDisplayId: "session-123", + sessionDisplayId: "12345678-1234-4abc-9def-123456789012", taskKey: null, }, config: { @@ -283,9 +283,9 @@ describe("claude remote execution", () => { adapterConfig: {}, }, runtime: { - sessionId: "session-123", + sessionId: "12345678-1234-4abc-9def-123456789012", sessionParams: { - sessionId: "session-123", + sessionId: "12345678-1234-4abc-9def-123456789012", cwd: managedRemoteWorkspace, remoteExecution: { transport: "ssh", @@ -295,7 +295,7 @@ describe("claude remote execution", () => { remoteCwd: managedRemoteWorkspace, }, }, - sessionDisplayId: "session-123", + sessionDisplayId: "12345678-1234-4abc-9def-123456789012", taskKey: null, }, config: { @@ -325,7 +325,7 @@ describe("claude remote execution", () => { expect(runChildProcess).toHaveBeenCalledTimes(1); const call = runChildProcess.mock.calls[0] as unknown as [string, string, string[]] | undefined; expect(call?.[2]).toContain("--resume"); - expect(call?.[2]).toContain("session-123"); + expect(call?.[2]).toContain("12345678-1234-4abc-9def-123456789012"); }); }); diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index 99fdd995..6086cc3e 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -598,8 +598,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 && + isValidUuid && hasMatchingPromptBundle && claudeSessionCwdMatchesExecutionTarget({ runtimeSessionCwd, @@ -608,9 +610,16 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 && path.resolve(runtimeSessionCwd) !== path.resolve(effectiveExecutionCwd) ) { @@ -626,7 +636,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise): bo .filter(Boolean); return allMessages.some((msg) => - /no conversation found with session id|unknown session|session .* not found/i.test(msg), + /no conversation found with session id|unknown session|session .* not found|not a valid UUID/i.test(msg), ); } diff --git a/server/src/__tests__/claude-local-execute.test.ts b/server/src/__tests__/claude-local-execute.test.ts index 52527051..6dba0ac8 100644 --- a/server/src/__tests__/claude-local-execute.test.ts +++ b/server/src/__tests__/claude-local-execute.test.ts @@ -66,9 +66,9 @@ const payload = { if (capturePath) { fs.writeFileSync(capturePath, JSON.stringify(payload), "utf8"); } -console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "claude-session-1", model: "claude-sonnet" })); -console.log(JSON.stringify({ type: "assistant", session_id: "claude-session-1", message: { content: [{ type: "text", text: "hello" }] } })); -console.log(JSON.stringify({ type: "result", session_id: "claude-session-1", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } })); +console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "11111111-1111-4111-8111-111111111111", model: "claude-sonnet" })); +console.log(JSON.stringify({ type: "assistant", session_id: "11111111-1111-4111-8111-111111111111", message: { content: [{ type: "text", text: "hello" }] } })); +console.log(JSON.stringify({ type: "result", session_id: "11111111-1111-4111-8111-111111111111", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } })); `; await fs.writeFile(commandPath, script, "utf8"); await fs.chmod(commandPath, 0o755); @@ -112,15 +112,15 @@ if (shouldFailResume) { console.log(JSON.stringify({ type: "result", subtype: "success", - session_id: "claude-session-poisoned", + session_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", is_error: true, result: "API Error: 400 diagnostics.previous_message_id: must be the \`id\` from a prior /v1/messages response (starts with \`msg_\`)", })); process.exit(1); } -console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "claude-session-new", model: "claude-sonnet" })); -console.log(JSON.stringify({ type: "assistant", session_id: "claude-session-new", message: { content: [{ type: "text", text: "hello" }] } })); -console.log(JSON.stringify({ type: "result", session_id: "claude-session-new", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } })); +console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", model: "claude-sonnet" })); +console.log(JSON.stringify({ type: "assistant", session_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", message: { content: [{ type: "text", text: "hello" }] } })); +console.log(JSON.stringify({ type: "result", session_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } })); `; await fs.writeFile(commandPath, script, "utf8"); await fs.chmod(commandPath, 0o755); @@ -146,7 +146,7 @@ if (capturePath) { console.log(JSON.stringify({ type: "result", subtype: "success", - session_id: "claude-session-poisoned-fresh", + session_id: "fffff111-0000-4000-8000-000000000003", is_error: true, result: "API Error: 400 diagnostics.previous_message_id: must be the \`id\` from a prior /v1/messages response (starts with \`msg_\`)", })); @@ -183,15 +183,15 @@ if (shouldFailResume) { console.log(JSON.stringify({ type: "result", subtype: "error", - session_id: "claude-session-1", - result: "No conversation found with session id claude-session-1", - errors: ["No conversation found with session id claude-session-1"], + session_id: "11111111-1111-4111-8111-111111111111", + result: "No conversation found with session id 11111111-1111-4111-8111-111111111111", + errors: ["No conversation found with session id 11111111-1111-4111-8111-111111111111"], })); process.exit(1); } -console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "claude-session-2", model: "claude-sonnet" })); -console.log(JSON.stringify({ type: "assistant", session_id: "claude-session-2", message: { content: [{ type: "text", text: "hello" }] } })); -console.log(JSON.stringify({ type: "result", session_id: "claude-session-2", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } })); +console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "22222222-2222-4222-8222-222222222222", model: "claude-sonnet" })); +console.log(JSON.stringify({ type: "assistant", session_id: "22222222-2222-4222-8222-222222222222", message: { content: [{ type: "text", text: "hello" }] } })); +console.log(JSON.stringify({ type: "result", session_id: "22222222-2222-4222-8222-222222222222", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } })); `; await fs.writeFile(commandPath, script, "utf8"); await fs.chmod(commandPath, 0o755); @@ -305,7 +305,7 @@ describe("claude execute", () => { await execute({ runId: "run-resume", agent: { id: "agent-1", companyId: "co-1", name: "Test", adapterType: "claude_local", adapterConfig: {} }, - runtime: { sessionId: "claude-session-1", sessionParams: null, sessionDisplayId: null, taskKey: null }, + runtime: { sessionId: "11111111-1111-4111-8111-111111111111", sessionParams: null, sessionDisplayId: null, taskKey: null }, config: { command: commandPath, cwd: workspace, @@ -373,7 +373,7 @@ describe("claude execute", () => { await execute({ runId: "run-notes-resume", agent: { id: "agent-1", companyId: "co-1", name: "Test", adapterType: "claude_local", adapterConfig: {} }, - runtime: { sessionId: "claude-session-1", sessionParams: null, sessionDisplayId: null, taskKey: null }, + runtime: { sessionId: "11111111-1111-4111-8111-111111111111", sessionParams: null, sessionDisplayId: null, taskKey: null }, config: { command: commandPath, cwd: workspace, @@ -405,7 +405,7 @@ describe("claude execute", () => { const result = await execute({ runId: "run-resume-fallback", agent: { id: "agent-1", companyId: "co-1", name: "Test", adapterType: "claude_local", adapterConfig: {} }, - runtime: { sessionId: "claude-session-1", sessionParams: null, sessionDisplayId: null, taskKey: null }, + runtime: { sessionId: "11111111-1111-4111-8111-111111111111", sessionParams: null, sessionDisplayId: null, taskKey: null }, config: { command: commandPath, cwd: workspace, @@ -448,7 +448,7 @@ describe("claude execute", () => { expect(metaEvents).toHaveLength(2); expect(metaEvents[0]?.commandNotes).toHaveLength(0); expect(metaEvents[1]?.commandNotes.some((note) => note.includes("--append-system-prompt-file"))).toBe(true); - expect(result.sessionId).toBe("claude-session-2"); + expect(result.sessionId).toBe("22222222-2222-4222-8222-222222222222"); expect(result.clearSession).toBe(false); } finally { restore(); @@ -461,7 +461,7 @@ describe("claude execute", () => { const resultEvent = { type: "result", subtype: "error_max_turns", - session_id: "claude-session-1", + session_id: "11111111-1111-4111-8111-111111111111", is_error: true, result: "Maximum turns reached.", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 }, @@ -501,7 +501,7 @@ describe("claude execute", () => { const resultEvent = { type: "result", subtype: "error", - session_id: "claude-session-1", + session_id: "11111111-1111-4111-8111-111111111111", is_error: true, result: "Tool output said: Maximum turns reached.", }; @@ -796,7 +796,7 @@ describe("claude execute", () => { expect(first.exitCode).toBe(0); expect(first.errorMessage).toBeNull(); expect(first.sessionParams).toMatchObject({ - sessionId: "claude-session-1", + sessionId: "11111111-1111-4111-8111-111111111111", cwd: workspace, }); expect(typeof first.sessionParams?.promptBundleKey).toBe("string"); @@ -888,7 +888,7 @@ describe("claude execute", () => { expect(capture1.instructionsContents).toContain(`The above agent instructions were loaded from ${instructionsPath}.`); expect(capture1.skillEntries).toContain("paperclip"); expect(capture2.argv).toContain("--resume"); - expect(capture2.argv).toContain("claude-session-1"); + expect(capture2.argv).toContain("11111111-1111-4111-8111-111111111111"); expect(capture2.prompt).toContain("## Paperclip Resume Delta"); expect(capture2.prompt).not.toContain("Follow the paperclip heartbeat."); } finally { @@ -1016,7 +1016,7 @@ describe("claude execute", () => { resultEvent: { type: "result", subtype: "error", - session_id: "claude-session-extra", + session_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc", is_error: true, result: "You're out of extra usage ยท resets 4pm (America/Chicago)", errors: [{ type: "rate_limit_error", message: "You're out of extra usage" }], @@ -1081,7 +1081,7 @@ describe("claude execute", () => { resultEvent: { type: "result", subtype: "error", - session_id: "claude-session-overloaded", + session_id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd", is_error: true, result: "Overloaded", errors: [{ type: "overloaded_error", message: "Overloaded_error: API is overloaded." }], @@ -1139,7 +1139,7 @@ describe("claude execute", () => { resultEvent: { type: "result", subtype: "error_max_turns", - session_id: "claude-session-max-turns", + session_id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", is_error: true, result: "Maximum turns reached.", }, @@ -1193,7 +1193,7 @@ describe("claude execute", () => { const result = await execute({ runId: "run-poisoned-msgid", agent: { id: "agent-1", companyId: "co-1", name: "Test", adapterType: "claude_local", adapterConfig: {} }, - runtime: { sessionId: "claude-session-poisoned", sessionParams: null, sessionDisplayId: null, taskKey: null }, + runtime: { sessionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", sessionParams: null, sessionDisplayId: null, taskKey: null }, config: { command: commandPath, cwd: workspace, @@ -1214,7 +1214,7 @@ describe("claude execute", () => { expect(captured[0]?.argv).toContain("--resume"); expect(captured[1]?.argv).not.toContain("--resume"); // Result comes from the fresh retry - expect(result.sessionId).toBe("claude-session-new"); + expect(result.sessionId).toBe("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"); expect(result.errorCode).toBeNull(); // Adapter logged the fallback reason expect(logs.some((l) => l.includes("poisoned message-id"))).toBe(true); @@ -1270,7 +1270,7 @@ describe("claude execute", () => { * Regression for RED-978: if the auto-retry after a poisoned resume *also* * fails with a poisoned previous_message_id, the adapter must still emit * clearSession=true so the next heartbeat starts from a clean transcript. - * Before this fix, the retry result's session_id ("claude-session-poisoned-fresh") + * Before this fix, the retry result's session_id ("fffff111-0000-4000-8000-000000000003") * was persisted and every subsequent continuation hit the same 400 again. */ it("forces clearSession when the recovery retry also reports a poisoned previous_message_id", async () => { @@ -1283,7 +1283,7 @@ describe("claude execute", () => { runId: "run-poisoned-retry", agent: { id: "agent-1", companyId: "co-1", name: "Test", adapterType: "claude_local", adapterConfig: {} }, runtime: { - sessionId: "claude-session-already-poisoned", + sessionId: "aaaaaaaa-0000-4000-8000-000000000004", sessionParams: null, sessionDisplayId: null, taskKey: null, diff --git a/server/src/secrets/local-encrypted-provider.ts b/server/src/secrets/local-encrypted-provider.ts index e19ccc47..33d3c0c1 100644 --- a/server/src/secrets/local-encrypted-provider.ts +++ b/server/src/secrets/local-encrypted-provider.ts @@ -211,8 +211,17 @@ function decryptValue(masterKey: Buffer, material: LocalEncryptedMaterial): stri const ciphertext = Buffer.from(material.ciphertext, "base64"); const decipher = createDecipheriv("aes-256-gcm", masterKey, iv); decipher.setAuthTag(tag); - const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]); - return plain.toString("utf8"); + try { + const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]); + return plain.toString("utf8"); + } catch (err) { + const keyFingerprint = createHash("sha256").update(masterKey).digest("hex").slice(0, 12); + throw new Error( + `Secret decryption failed (master key fingerprint: ${keyFingerprint}). ` + + `This usually means the master key does not match the key used to encrypt the secret. ` + + `Original error: ${err instanceof Error ? err.message : String(err)}`, + ); + } } function asLocalEncryptedMaterial(value: StoredSecretVersionMaterial): LocalEncryptedMaterial { diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 88f02451..7b2da065 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -9255,11 +9255,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const setupFailureAgent = await getAgent(run.agentId).catch(() => null); await setRunStatus(runId, "failed", { error: message, - errorCode: "adapter_failed", + errorCode: "setup_failed", finishedAt: new Date(), ...(setupFailureAgent ? { resultJson: mergeRunStopMetadataForAgent(setupFailureAgent, "failed", { - errorCode: "adapter_failed", + errorCode: "setup_failed", errorMessage: message, }), } : {}),