mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix: make claude live output limits configurable
This commit is contained in:
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/pricing: abort in-flight model pricing catalog fetches when Gateway shutdown stops the refresh loop, and avoid post-stop cache writes or refresh timers. Fixes #72208. Thanks @rzcq.
|
||||
- Control UI/Talk: allow the OpenAI Realtime WebRTC offer endpoint through the Control UI CSP, configure browser sessions with explicit VAD/transcription input settings, and surface OpenAI realtime error/lifecycle events instead of leaving Talk stuck as live with no diagnostic. Fixes #73427.
|
||||
- Plugins: clarify config-selected duplicate plugin override diagnostics and document manifest schema updates for bundled-plugin forks. Fixes #8582. Thanks @sachah.
|
||||
- CLI backends/Claude: make live-session JSONL turn caps bounded and configurable via `reliability.outputLimits`, raising the default guard for tool-heavy Claude CLI turns while preserving memory limits. Fixes #75838. Thanks @hcordoba840.
|
||||
- Providers/OpenAI: resolve `keychain:<service>:<account>` `OPENAI_API_KEY` refs before creating OpenAI Realtime browser sessions or voice bridges, with a bounded cached Keychain lookup. Fixes #72120. Thanks @ctbritt.
|
||||
- Discord/gateway: reconnect when the gateway socket closes while waiting for the shared IDENTIFY concurrency window, instead of silently skipping IDENTIFY and leaving the bot online but unresponsive. Fixes #74617. Thanks @zeeskdr-ai.
|
||||
- Voice Call: add `sessionScope: "per-call"` for fresh per-call agent memory while preserving the default per-phone caller history. Fixes #45280. Thanks @pondcountry.
|
||||
|
||||
@@ -210,6 +210,12 @@ binary is not already on `PATH`.
|
||||
ids are verified against an existing readable project transcript before
|
||||
resume, so phantom bindings are cleared with `reason=transcript-missing`
|
||||
instead of silently starting a fresh Claude CLI session under `--resume`.
|
||||
- Claude live sessions keep bounded JSONL output guards. Defaults allow up to
|
||||
8 MiB and 20,000 raw JSONL lines per turn. Tool-heavy Claude turns can raise
|
||||
them per backend with
|
||||
`agents.defaults.cliBackends.claude-cli.reliability.outputLimits.maxTurnRawChars`
|
||||
and `maxTurnLines`; OpenClaw clamps those settings to 64 MiB and 100,000
|
||||
lines.
|
||||
- Stored CLI sessions are provider-owned continuity. The implicit daily session
|
||||
reset does not cut them; `/reset` and explicit `session.reset` policies still
|
||||
do.
|
||||
|
||||
@@ -430,6 +430,48 @@ describe("resolveCliBackendConfig reliability merge", () => {
|
||||
expect(resolved?.config.reliability?.watchdog?.resume?.maxMs).toBe(180_000);
|
||||
expect(resolved?.config.reliability?.watchdog?.fresh?.noOutputTimeoutRatio).toBe(0.8);
|
||||
});
|
||||
|
||||
it("deep-merges reliability output-limit overrides", () => {
|
||||
runtimeBackendEntries.unshift(
|
||||
createRuntimeBackendEntry({
|
||||
pluginId: "test",
|
||||
id: "test-cli",
|
||||
config: {
|
||||
command: "test-cli",
|
||||
reliability: {
|
||||
outputLimits: {
|
||||
maxTurnRawChars: 8192,
|
||||
maxTurnLines: 20_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
cliBackends: {
|
||||
"test-cli": {
|
||||
command: "test-cli",
|
||||
reliability: {
|
||||
outputLimits: {
|
||||
maxTurnRawChars: 16_384,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
const resolved = resolveCliBackendConfig("test-cli", cfg);
|
||||
|
||||
expect(resolved).not.toBeNull();
|
||||
expect(resolved?.config.reliability?.outputLimits).toEqual({
|
||||
maxTurnRawChars: 16_384,
|
||||
maxTurnLines: 20_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveCliBackendLiveTest", () => {
|
||||
|
||||
@@ -142,8 +142,10 @@ function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig)
|
||||
}
|
||||
const baseFresh = base.reliability?.watchdog?.fresh ?? {};
|
||||
const baseResume = base.reliability?.watchdog?.resume ?? {};
|
||||
const baseOutputLimits = base.reliability?.outputLimits ?? {};
|
||||
const overrideFresh = override.reliability?.watchdog?.fresh ?? {};
|
||||
const overrideResume = override.reliability?.watchdog?.resume ?? {};
|
||||
const overrideOutputLimits = override.reliability?.outputLimits ?? {};
|
||||
return {
|
||||
...base,
|
||||
...override,
|
||||
@@ -157,6 +159,10 @@ function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig)
|
||||
reliability: {
|
||||
...base.reliability,
|
||||
...override.reliability,
|
||||
outputLimits: {
|
||||
...baseOutputLimits,
|
||||
...overrideOutputLimits,
|
||||
},
|
||||
watchdog: {
|
||||
...base.reliability?.watchdog,
|
||||
...override.reliability?.watchdog,
|
||||
|
||||
@@ -957,6 +957,106 @@ describe("runCliAgent spawn path", () => {
|
||||
expect(result.text).toBe(largeText);
|
||||
});
|
||||
|
||||
it("honors configured Claude live stream-json raw turn limits", async () => {
|
||||
const largeText = "x".repeat(1500);
|
||||
let stdoutListener: ((chunk: string) => void) | undefined;
|
||||
const stdin = {
|
||||
write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
|
||||
stdoutListener?.(
|
||||
JSON.stringify({
|
||||
type: "result",
|
||||
session_id: "live-session-tight-output-limit",
|
||||
result: largeText,
|
||||
}) + "\n",
|
||||
);
|
||||
cb?.();
|
||||
}),
|
||||
end: vi.fn(),
|
||||
};
|
||||
supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
|
||||
const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
|
||||
stdoutListener = input.onStdout;
|
||||
return {
|
||||
runId: "live-run-tight-output-limit",
|
||||
pid: 2345,
|
||||
startedAtMs: Date.now(),
|
||||
stdin,
|
||||
wait: vi.fn(() => new Promise(() => {})),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
await expect(
|
||||
executePreparedCliRun(
|
||||
buildPreparedCliRunContext({
|
||||
provider: "claude-cli",
|
||||
model: "sonnet",
|
||||
runId: "run-live-tight-output-limit",
|
||||
backend: {
|
||||
liveSession: "claude-stdio",
|
||||
reliability: {
|
||||
outputLimits: {
|
||||
maxTurnRawChars: 1024,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
name: "FailoverError",
|
||||
message: "Claude CLI JSONL line exceeded output limit.",
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts operator-raised Claude live stream-json raw turn limits", async () => {
|
||||
const largeText = "x".repeat(1500);
|
||||
let stdoutListener: ((chunk: string) => void) | undefined;
|
||||
const stdin = {
|
||||
write: vi.fn((_data: string, cb?: (err?: Error | null) => void) => {
|
||||
stdoutListener?.(
|
||||
JSON.stringify({
|
||||
type: "result",
|
||||
session_id: "live-session-raised-output-limit",
|
||||
result: largeText,
|
||||
}) + "\n",
|
||||
);
|
||||
cb?.();
|
||||
}),
|
||||
end: vi.fn(),
|
||||
};
|
||||
supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
|
||||
const input = (args[0] ?? {}) as { onStdout?: (chunk: string) => void };
|
||||
stdoutListener = input.onStdout;
|
||||
return {
|
||||
runId: "live-run-raised-output-limit",
|
||||
pid: 2345,
|
||||
startedAtMs: Date.now(),
|
||||
stdin,
|
||||
wait: vi.fn(() => new Promise(() => {})),
|
||||
cancel: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const result = await executePreparedCliRun(
|
||||
buildPreparedCliRunContext({
|
||||
provider: "claude-cli",
|
||||
model: "sonnet",
|
||||
runId: "run-live-raised-output-limit",
|
||||
backend: {
|
||||
liveSession: "claude-stdio",
|
||||
reliability: {
|
||||
outputLimits: {
|
||||
maxTurnRawChars: 4096,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.text).toHaveLength(largeText.length);
|
||||
expect(result.text).toBe(largeText);
|
||||
});
|
||||
|
||||
it("reports Claude live session reply backends as streaming until the turn finishes", async () => {
|
||||
let stdoutListener: ((chunk: string) => void) | undefined;
|
||||
let markWriteReady: (() => void) | undefined;
|
||||
|
||||
@@ -19,6 +19,7 @@ type ProcessSupervisor = ReturnType<
|
||||
type ManagedRun = Awaited<ReturnType<ProcessSupervisor["spawn"]>>;
|
||||
type ClaudeLiveTurn = {
|
||||
backend: CliBackendConfig;
|
||||
outputLimits: ClaudeLiveOutputLimits;
|
||||
startedAtMs: number;
|
||||
rawLines: string[];
|
||||
rawChars: number;
|
||||
@@ -49,13 +50,21 @@ type ClaudeLiveSession = {
|
||||
type ClaudeLiveRunResult = {
|
||||
output: CliOutput;
|
||||
};
|
||||
type ClaudeLiveOutputLimits = {
|
||||
maxTurnRawChars: number;
|
||||
maxPendingLineChars: number;
|
||||
maxTurnLines: number;
|
||||
};
|
||||
|
||||
const CLAUDE_LIVE_IDLE_TIMEOUT_MS = 10 * 60 * 1_000;
|
||||
const CLAUDE_LIVE_MAX_SESSIONS = 16;
|
||||
const CLAUDE_LIVE_MAX_STDERR_CHARS = 64 * 1024;
|
||||
const CLAUDE_LIVE_MAX_TURN_RAW_CHARS = 2 * 1024 * 1024;
|
||||
const CLAUDE_LIVE_MAX_PENDING_LINE_CHARS = CLAUDE_LIVE_MAX_TURN_RAW_CHARS;
|
||||
const CLAUDE_LIVE_MAX_TURN_LINES = 5_000;
|
||||
const CLAUDE_LIVE_DEFAULT_MAX_TURN_RAW_CHARS = 8 * 1024 * 1024;
|
||||
const CLAUDE_LIVE_MIN_TURN_RAW_CHARS = 1_024;
|
||||
const CLAUDE_LIVE_MAX_CONFIGURABLE_TURN_RAW_CHARS = 64 * 1024 * 1024;
|
||||
const CLAUDE_LIVE_DEFAULT_MAX_TURN_LINES = 20_000;
|
||||
const CLAUDE_LIVE_MIN_TURN_LINES = 100;
|
||||
const CLAUDE_LIVE_MAX_CONFIGURABLE_TURN_LINES = 100_000;
|
||||
const CLAUDE_LIVE_CLOSE_WAIT_TIMEOUT_MS = 5_000;
|
||||
const liveSessions = new Map<string, ClaudeLiveSession>();
|
||||
const liveSessionCreates = new Map<string, Promise<ClaudeLiveSession>>();
|
||||
@@ -439,11 +448,45 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalizePositiveInt(
|
||||
value: number | undefined,
|
||||
fallback: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): number {
|
||||
if (!Number.isInteger(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function resolveClaudeLiveOutputLimits(backend: CliBackendConfig): ClaudeLiveOutputLimits {
|
||||
const configured = backend.reliability?.outputLimits;
|
||||
const maxTurnRawChars = normalizePositiveInt(
|
||||
configured?.maxTurnRawChars,
|
||||
CLAUDE_LIVE_DEFAULT_MAX_TURN_RAW_CHARS,
|
||||
CLAUDE_LIVE_MIN_TURN_RAW_CHARS,
|
||||
CLAUDE_LIVE_MAX_CONFIGURABLE_TURN_RAW_CHARS,
|
||||
);
|
||||
return {
|
||||
maxTurnRawChars,
|
||||
maxPendingLineChars: maxTurnRawChars,
|
||||
maxTurnLines: normalizePositiveInt(
|
||||
configured?.maxTurnLines,
|
||||
CLAUDE_LIVE_DEFAULT_MAX_TURN_LINES,
|
||||
CLAUDE_LIVE_MIN_TURN_LINES,
|
||||
CLAUDE_LIVE_MAX_CONFIGURABLE_TURN_LINES,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseClaudeLiveJsonLine(
|
||||
session: ClaudeLiveSession,
|
||||
trimmed: string,
|
||||
): Record<string, unknown> | null {
|
||||
if (trimmed.length > CLAUDE_LIVE_MAX_PENDING_LINE_CHARS) {
|
||||
const maxPendingLineChars =
|
||||
session.currentTurn?.outputLimits.maxPendingLineChars ?? CLAUDE_LIVE_DEFAULT_MAX_TURN_RAW_CHARS;
|
||||
if (trimmed.length > maxPendingLineChars) {
|
||||
closeLiveSession(
|
||||
session,
|
||||
"abort",
|
||||
@@ -504,8 +547,8 @@ function handleClaudeLiveLine(session: ClaudeLiveSession, line: string): void {
|
||||
}
|
||||
turn.rawChars += trimmed.length + 1;
|
||||
if (
|
||||
turn.rawChars > CLAUDE_LIVE_MAX_TURN_RAW_CHARS ||
|
||||
turn.rawLines.length >= CLAUDE_LIVE_MAX_TURN_LINES
|
||||
turn.rawChars > turn.outputLimits.maxTurnRawChars ||
|
||||
turn.rawLines.length >= turn.outputLimits.maxTurnLines
|
||||
) {
|
||||
closeLiveSession(
|
||||
session,
|
||||
@@ -541,7 +584,9 @@ function handleClaudeLiveLine(session: ClaudeLiveSession, line: string): void {
|
||||
function handleClaudeStdout(session: ClaudeLiveSession, chunk: string) {
|
||||
resetNoOutputTimer(session);
|
||||
session.stdoutBuffer += chunk;
|
||||
if (session.stdoutBuffer.length > CLAUDE_LIVE_MAX_PENDING_LINE_CHARS) {
|
||||
const maxPendingLineChars =
|
||||
session.currentTurn?.outputLimits.maxPendingLineChars ?? CLAUDE_LIVE_DEFAULT_MAX_TURN_RAW_CHARS;
|
||||
if (session.stdoutBuffer.length > maxPendingLineChars) {
|
||||
closeLiveSession(
|
||||
session,
|
||||
"abort",
|
||||
@@ -719,6 +764,7 @@ function createTurn(params: {
|
||||
}): ClaudeLiveTurn {
|
||||
const turn: ClaudeLiveTurn = {
|
||||
backend: params.context.preparedBackend.backend,
|
||||
outputLimits: resolveClaudeLiveOutputLimits(params.context.preparedBackend.backend),
|
||||
startedAtMs: Date.now(),
|
||||
rawLines: [],
|
||||
rawChars: 0,
|
||||
|
||||
@@ -149,6 +149,13 @@ export type CliBackendConfig = {
|
||||
serialize?: boolean;
|
||||
/** Runtime reliability tuning for this backend's process lifecycle. */
|
||||
reliability?: {
|
||||
/** Live-session output caps for CLIs that stream JSONL through a long-lived process. */
|
||||
outputLimits?: {
|
||||
/** Max raw JSONL characters retained for one live CLI turn. */
|
||||
maxTurnRawChars?: number;
|
||||
/** Max raw JSONL lines retained for one live CLI turn. */
|
||||
maxTurnLines?: number;
|
||||
};
|
||||
/** No-output watchdog tuning (fresh vs resumed runs). */
|
||||
watchdog?: {
|
||||
/** Fresh/new sessions (non-resume). */
|
||||
|
||||
@@ -571,6 +571,19 @@ const CliBackendWatchdogModeSchema = z
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
const CliBackendOutputLimitsSchema = z
|
||||
.object({
|
||||
maxTurnRawChars: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1024)
|
||||
.max(64 * 1024 * 1024)
|
||||
.optional(),
|
||||
maxTurnLines: z.number().int().min(100).max(100_000).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const CliBackendSchema = z
|
||||
.object({
|
||||
command: z.string(),
|
||||
@@ -606,6 +619,7 @@ export const CliBackendSchema = z
|
||||
serialize: z.boolean().optional(),
|
||||
reliability: z
|
||||
.object({
|
||||
outputLimits: CliBackendOutputLimitsSchema,
|
||||
watchdog: z
|
||||
.object({
|
||||
fresh: CliBackendWatchdogModeSchema,
|
||||
|
||||
Reference in New Issue
Block a user