feat(adapter): claude local chrome flag and max-turns session handling
Add --chrome flag support for Claude adapter. Detect max-turns exhaustion (via subtype, stop_reason, or result text) and clear the session to prevent stale session re-use. Add unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -160,6 +160,7 @@ export interface CreateConfigValues {
|
|||||||
promptTemplate: string;
|
promptTemplate: string;
|
||||||
model: string;
|
model: string;
|
||||||
thinkingEffort: string;
|
thinkingEffort: string;
|
||||||
|
chrome: boolean;
|
||||||
dangerouslySkipPermissions: boolean;
|
dangerouslySkipPermissions: boolean;
|
||||||
search: boolean;
|
search: boolean;
|
||||||
dangerouslyBypassSandbox: boolean;
|
dangerouslyBypassSandbox: boolean;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Core fields:
|
|||||||
- cwd (string, optional): default absolute working directory fallback for the agent process
|
- cwd (string, optional): default absolute working directory fallback for the agent process
|
||||||
- model (string, optional): Claude model id
|
- model (string, optional): Claude model id
|
||||||
- effort (string, optional): reasoning effort passed via --effort (low|medium|high)
|
- effort (string, optional): reasoning effort passed via --effort (low|medium|high)
|
||||||
|
- chrome (boolean, optional): pass --chrome when running Claude
|
||||||
- promptTemplate (string, optional): run prompt template
|
- promptTemplate (string, optional): run prompt template
|
||||||
- bootstrapPromptTemplate (string, optional): first-run prompt template
|
- bootstrapPromptTemplate (string, optional): first-run prompt template
|
||||||
- maxTurnsPerRun (number, optional): max turns for one run
|
- maxTurnsPerRun (number, optional): max turns for one run
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
parseClaudeStreamJson,
|
parseClaudeStreamJson,
|
||||||
describeClaudeFailure,
|
describeClaudeFailure,
|
||||||
detectClaudeLoginRequired,
|
detectClaudeLoginRequired,
|
||||||
|
isClaudeMaxTurnsResult,
|
||||||
isClaudeUnknownSessionError,
|
isClaudeUnknownSessionError,
|
||||||
} from "./parse.js";
|
} from "./parse.js";
|
||||||
|
|
||||||
@@ -263,6 +264,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
const bootstrapTemplate = asString(config.bootstrapPromptTemplate, promptTemplate);
|
const bootstrapTemplate = asString(config.bootstrapPromptTemplate, promptTemplate);
|
||||||
const model = asString(config.model, "");
|
const model = asString(config.model, "");
|
||||||
const effort = asString(config.effort, "");
|
const effort = asString(config.effort, "");
|
||||||
|
const chrome = asBoolean(config.chrome, false);
|
||||||
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
|
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
|
||||||
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false);
|
const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false);
|
||||||
|
|
||||||
@@ -315,6 +317,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
const args = ["--print", "-", "--output-format", "stream-json", "--verbose"];
|
const args = ["--print", "-", "--output-format", "stream-json", "--verbose"];
|
||||||
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
||||||
if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
|
if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
|
||||||
|
if (chrome) args.push("--chrome");
|
||||||
if (model) args.push("--model", model);
|
if (model) args.push("--model", model);
|
||||||
if (effort) args.push("--effort", effort);
|
if (effort) args.push("--effort", effort);
|
||||||
if (maxTurns > 0) args.push("--max-turns", String(maxTurns));
|
if (maxTurns > 0) args.push("--max-turns", String(maxTurns));
|
||||||
@@ -439,6 +442,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
||||||
} as Record<string, unknown>)
|
} as Record<string, unknown>)
|
||||||
: null;
|
: null;
|
||||||
|
const clearSessionForMaxTurns = isClaudeMaxTurnsResult(parsed);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exitCode: proc.exitCode,
|
exitCode: proc.exitCode,
|
||||||
@@ -460,7 +464,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||||||
costUsd: parsedStream.costUsd ?? asNumber(parsed.total_cost_usd, 0),
|
costUsd: parsedStream.costUsd ?? asNumber(parsed.total_cost_usd, 0),
|
||||||
resultJson: parsed,
|
resultJson: parsed,
|
||||||
summary: parsedStream.summary || asString(parsed.result, ""),
|
summary: parsedStream.summary || asString(parsed.result, ""),
|
||||||
clearSession: Boolean(opts.clearSessionOnMissingSession && !resolvedSessionId),
|
clearSession: clearSessionForMaxTurns || Boolean(opts.clearSessionOnMissingSession && !resolvedSessionId),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
export { execute, runClaudeLogin } from "./execute.js";
|
export { execute, runClaudeLogin } from "./execute.js";
|
||||||
export { testEnvironment } from "./test.js";
|
export { testEnvironment } from "./test.js";
|
||||||
export { parseClaudeStreamJson, describeClaudeFailure, isClaudeUnknownSessionError } from "./parse.js";
|
export {
|
||||||
|
parseClaudeStreamJson,
|
||||||
|
describeClaudeFailure,
|
||||||
|
isClaudeMaxTurnsResult,
|
||||||
|
isClaudeUnknownSessionError,
|
||||||
|
} from "./parse.js";
|
||||||
import type { AdapterSessionCodec } from "@paperclip/adapter-utils";
|
import type { AdapterSessionCodec } from "@paperclip/adapter-utils";
|
||||||
|
|
||||||
function readNonEmptyString(value: unknown): string | null {
|
function readNonEmptyString(value: unknown): string | null {
|
||||||
|
|||||||
@@ -154,6 +154,19 @@ export function describeClaudeFailure(parsed: Record<string, unknown>): string |
|
|||||||
return parts.length > 1 ? parts.join(": ") : null;
|
return parts.length > 1 ? parts.join(": ") : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isClaudeMaxTurnsResult(parsed: Record<string, unknown> | null | undefined): boolean {
|
||||||
|
if (!parsed) return false;
|
||||||
|
|
||||||
|
const subtype = asString(parsed.subtype, "").trim().toLowerCase();
|
||||||
|
if (subtype === "error_max_turns") return true;
|
||||||
|
|
||||||
|
const stopReason = asString(parsed.stop_reason, "").trim().toLowerCase();
|
||||||
|
if (stopReason === "max_turns") return true;
|
||||||
|
|
||||||
|
const resultText = asString(parsed.result, "").trim();
|
||||||
|
return /max(?:imum)?\s+turns?/i.test(resultText);
|
||||||
|
}
|
||||||
|
|
||||||
export function isClaudeUnknownSessionError(parsed: Record<string, unknown>): boolean {
|
export function isClaudeUnknownSessionError(parsed: Record<string, unknown>): boolean {
|
||||||
const resultText = asString(parsed.result, "").trim();
|
const resultText = asString(parsed.result, "").trim();
|
||||||
const allMessages = [resultText, ...extractClaudeErrorMessages(parsed)]
|
const allMessages = [resultText, ...extractClaudeErrorMessages(parsed)]
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export function buildClaudeLocalConfig(v: CreateConfigValues): Record<string, un
|
|||||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||||
if (v.model) ac.model = v.model;
|
if (v.model) ac.model = v.model;
|
||||||
if (v.thinkingEffort) ac.effort = v.thinkingEffort;
|
if (v.thinkingEffort) ac.effort = v.thinkingEffort;
|
||||||
|
if (v.chrome) ac.chrome = true;
|
||||||
ac.timeoutSec = 0;
|
ac.timeoutSec = 0;
|
||||||
ac.graceSec = 15;
|
ac.graceSec = 15;
|
||||||
const env = parseEnvBindings(v.envBindings);
|
const env = parseEnvBindings(v.envBindings);
|
||||||
|
|||||||
30
server/src/__tests__/claude-local-adapter.test.ts
Normal file
30
server/src/__tests__/claude-local-adapter.test.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { isClaudeMaxTurnsResult } from "@paperclip/adapter-claude-local/server";
|
||||||
|
|
||||||
|
describe("claude_local max-turn detection", () => {
|
||||||
|
it("detects max-turn exhaustion by subtype", () => {
|
||||||
|
expect(
|
||||||
|
isClaudeMaxTurnsResult({
|
||||||
|
subtype: "error_max_turns",
|
||||||
|
result: "Reached max turns",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects max-turn exhaustion by stop_reason", () => {
|
||||||
|
expect(
|
||||||
|
isClaudeMaxTurnsResult({
|
||||||
|
stop_reason: "max_turns",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for non-max-turn results", () => {
|
||||||
|
expect(
|
||||||
|
isClaudeMaxTurnsResult({
|
||||||
|
subtype: "success",
|
||||||
|
stop_reason: "end_turn",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,6 +24,20 @@ export function ClaudeLocalAdvancedFields({
|
|||||||
}: AdapterConfigFieldsProps) {
|
}: AdapterConfigFieldsProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ToggleField
|
||||||
|
label="Enable Chrome"
|
||||||
|
hint={help.chrome}
|
||||||
|
checked={
|
||||||
|
isCreate
|
||||||
|
? values!.chrome
|
||||||
|
: eff("adapterConfig", "chrome", config.chrome === true)
|
||||||
|
}
|
||||||
|
onChange={(v) =>
|
||||||
|
isCreate
|
||||||
|
? set!({ chrome: v })
|
||||||
|
: mark("adapterConfig", "chrome", v)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ToggleField
|
<ToggleField
|
||||||
label="Skip permissions"
|
label="Skip permissions"
|
||||||
hint={help.dangerouslySkipPermissions}
|
hint={help.dangerouslySkipPermissions}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const defaultCreateValues: CreateConfigValues = {
|
|||||||
promptTemplate: "",
|
promptTemplate: "",
|
||||||
model: "",
|
model: "",
|
||||||
thinkingEffort: "",
|
thinkingEffort: "",
|
||||||
|
chrome: false,
|
||||||
dangerouslySkipPermissions: false,
|
dangerouslySkipPermissions: false,
|
||||||
search: false,
|
search: false,
|
||||||
dangerouslyBypassSandbox: false,
|
dangerouslyBypassSandbox: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user