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;
|
||||
model: string;
|
||||
thinkingEffort: string;
|
||||
chrome: boolean;
|
||||
dangerouslySkipPermissions: boolean;
|
||||
search: boolean;
|
||||
dangerouslyBypassSandbox: boolean;
|
||||
|
||||
@@ -15,6 +15,7 @@ Core fields:
|
||||
- cwd (string, optional): default absolute working directory fallback for the agent process
|
||||
- model (string, optional): Claude model id
|
||||
- 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
|
||||
- bootstrapPromptTemplate (string, optional): first-run prompt template
|
||||
- maxTurnsPerRun (number, optional): max turns for one run
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
parseClaudeStreamJson,
|
||||
describeClaudeFailure,
|
||||
detectClaudeLoginRequired,
|
||||
isClaudeMaxTurnsResult,
|
||||
isClaudeUnknownSessionError,
|
||||
} from "./parse.js";
|
||||
|
||||
@@ -263,6 +264,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const bootstrapTemplate = asString(config.bootstrapPromptTemplate, promptTemplate);
|
||||
const model = asString(config.model, "");
|
||||
const effort = asString(config.effort, "");
|
||||
const chrome = asBoolean(config.chrome, false);
|
||||
const maxTurns = asNumber(config.maxTurnsPerRun, 0);
|
||||
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"];
|
||||
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
||||
if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
|
||||
if (chrome) args.push("--chrome");
|
||||
if (model) args.push("--model", model);
|
||||
if (effort) args.push("--effort", effort);
|
||||
if (maxTurns > 0) args.push("--max-turns", String(maxTurns));
|
||||
@@ -439,6 +442,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
||||
} as Record<string, unknown>)
|
||||
: null;
|
||||
const clearSessionForMaxTurns = isClaudeMaxTurnsResult(parsed);
|
||||
|
||||
return {
|
||||
exitCode: proc.exitCode,
|
||||
@@ -460,7 +464,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
costUsd: parsedStream.costUsd ?? asNumber(parsed.total_cost_usd, 0),
|
||||
resultJson: parsed,
|
||||
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 { 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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
const resultText = asString(parsed.result, "").trim();
|
||||
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.model) ac.model = v.model;
|
||||
if (v.thinkingEffort) ac.effort = v.thinkingEffort;
|
||||
if (v.chrome) ac.chrome = true;
|
||||
ac.timeoutSec = 0;
|
||||
ac.graceSec = 15;
|
||||
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) {
|
||||
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
|
||||
label="Skip permissions"
|
||||
hint={help.dangerouslySkipPermissions}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const defaultCreateValues: CreateConfigValues = {
|
||||
promptTemplate: "",
|
||||
model: "",
|
||||
thinkingEffort: "",
|
||||
chrome: false,
|
||||
dangerouslySkipPermissions: false,
|
||||
search: false,
|
||||
dangerouslyBypassSandbox: false,
|
||||
|
||||
Reference in New Issue
Block a user