惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
WordPress大学
WordPress大学
阮一峰的网络日志
阮一峰的网络日志
博客园 - 司徒正美
月光博客
月光博客
宝玉的分享
宝玉的分享
Recent Announcements
Recent Announcements
小众软件
小众软件
H
Hackread – Cybersecurity News, Data Breaches, AI and More
美团技术团队
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
J
Java Code Geeks
云风的 BLOG
云风的 BLOG
罗磊的独立博客
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
Vercel News
Vercel News
量子位
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
腾讯CDC
有赞技术团队
有赞技术团队

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix empty cli response handling · openclaw/openclaw@76ce72c
joshavant · 2026-05-18 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -160,6 +160,7 @@ describe("runCliAgent cron before_agent_reply seam", () => {

160160

const { runCliAgent } = await import("./cli-runner.js");

161161

hasHooksMock.mockImplementation((hookName) => hookName === "before_agent_reply");

162162

runBeforeAgentReplyMock.mockResolvedValue(undefined);

163+

executePreparedCliRunMock.mockResolvedValue({ text: "real reply" });

163164

const onExecutionPhase = vi.fn();

164165
165166

await runCliAgent({

@@ -183,6 +184,19 @@ describe("runCliAgent cron before_agent_reply seam", () => {

183184

expect(executePreparedCliRunMock).toHaveBeenCalledTimes(1);

184185

});

185186
187+

it("treats empty CLI subprocess output as a failover failure, not a green cron run", async () => {

188+

const { runCliAgent } = await import("./cli-runner.js");

189+

executePreparedCliRunMock.mockResolvedValue({ text: " " });

190+
191+

await expect(runCliAgent({ ...baseRunParams, trigger: "cron" })).rejects.toMatchObject({

192+

name: "FailoverError",

193+

reason: "empty_response",

194+

provider: baseRunParams.provider,

195+

model: baseRunParams.model,

196+

sessionId: baseRunParams.sessionId,

197+

});

198+

});

199+
186200

it("returns a silent payload when a cron hook claims without a reply body", async () => {

187201

const { runCliAgent } = await import("./cli-runner.js");

188202

hasHooksMock.mockImplementation((hookName) => hookName === "before_agent_reply");

Original file line numberDiff line numberDiff line change

@@ -426,6 +426,48 @@ describe("runPreparedCliAgent context engine lifecycle", () => {

426426

expect(dispose).not.toHaveBeenCalled();

427427

});

428428
429+

it("does not finalize context-engine turns for empty successful CLI output", async () => {

430+

executePreparedCliRunMock.mockResolvedValue({

431+

text: " ",

432+

rawText: " ",

433+

sessionId: "external-cli-session-empty",

434+

usage: { input: 11, output: 0, total: 11 },

435+

});

436+

const bootstrap = vi.fn<NonNullable<ContextEngine["bootstrap"]>>(async () => ({

437+

bootstrapped: true,

438+

}));

439+

const afterTurn = vi.fn<NonNullable<ContextEngine["afterTurn"]>>(async () => {});

440+

const ingestBatch = vi.fn<NonNullable<ContextEngine["ingestBatch"]>>(async () => ({

441+

ingestedCount: 0,

442+

}));

443+

const maintain = vi.fn<NonNullable<ContextEngine["maintain"]>>(async () =>

444+

createMaintenanceResult(),

445+

);

446+

const dispose = vi.fn(async () => {});

447+

const contextEngine = createContextEngine({

448+

bootstrap,

449+

afterTurn,

450+

ingestBatch,

451+

maintain,

452+

dispose,

453+

});

454+

const { runPreparedCliAgent } = await import("./cli-runner.js");

455+
456+

await expect(runPreparedCliAgent(buildPreparedContext(contextEngine))).rejects.toMatchObject({

457+

name: "FailoverError",

458+

reason: "empty_response",

459+

provider: "claude-cli",

460+

model: "sonnet-4.6",

461+

sessionId: "openclaw-session-1",

462+

});

463+
464+

expect(bootstrap).toHaveBeenCalledTimes(1);

465+

expect(afterTurn).not.toHaveBeenCalled();

466+

expect(ingestBatch).not.toHaveBeenCalled();

467+

expect(maintain).toHaveBeenCalledTimes(1);

468+

expect(dispose).not.toHaveBeenCalled();

469+

});

470+
429471

it("does not dispose context engines when CLI attempts fail", async () => {

430472

executePreparedCliRunMock.mockRejectedValue(new Error("cli boom"));

431473

const dispose = vi.fn(async () => {

Original file line numberDiff line numberDiff line change

@@ -402,6 +402,15 @@ export async function runPreparedCliAgent(

402402

const executeCliAttempt = async (cliSessionIdToUse?: string) => {

403403

const output = await executePreparedCliRun(context, cliSessionIdToUse);

404404

const assistantText = output.text.trim();

405+

if (!assistantText) {

406+

throw new FailoverError("CLI backend returned an empty response.", {

407+

reason: "empty_response",

408+

provider: params.provider,

409+

model: context.modelId,

410+

sessionId: params.sessionId,

411+

lane: params.lane,

412+

});

413+

}

405414

const assistantTexts = assistantText ? [assistantText] : [];

406415

const lastAssistant =

407416

assistantText.length > 0

Original file line numberDiff line numberDiff line change

@@ -45,6 +45,20 @@ describe("resolveAuthProfileFailureReason", () => {

4545

).toBeNull();

4646

});

4747
48+

it("does not persist empty responses as auth-profile health", () => {

49+

expect(

50+

resolveAuthProfileFailureReason({

51+

failoverReason: "empty_response",

52+

}),

53+

).toBeNull();

54+

expect(

55+

resolveAuthProfileFailureReason({

56+

failoverReason: "empty_response",

57+

policy: "shared",

58+

}),

59+

).toBeNull();

60+

});

61+
4862

it("does not persist request-shape (format) rejections as auth-profile health (#77228)", () => {

4963

// A format rejection (e.g. the github-copilot prefill-strict 400

5064

// "conversation must end with a user message" reported in #77228) is

Original file line numberDiff line numberDiff line change

@@ -6,7 +6,7 @@ export function resolveAuthProfileFailureReason(params: {

66

failoverReason: FailoverReason | null;

77

policy?: AuthProfileFailurePolicy;

88

}): AuthProfileFailureReason | null {

9-

// Helper-local runs, transport/server failures, and request-shape ("format") rejections

9+

// Helper-local runs, transport/server failures, empty responses, and request-shape ("format") rejections

1010

// should not poison shared provider auth health. A `format` failure means the

1111

// provider rejected the request payload (e.g. an assistant-prefill 400 from a

1212

// strict provider when a session transcript ends with a stream-error placeholder

@@ -20,6 +20,7 @@ export function resolveAuthProfileFailureReason(params: {

2020

!params.failoverReason ||

2121

params.failoverReason === "timeout" ||

2222

params.failoverReason === "server_error" ||

23+

params.failoverReason === "empty_response" ||

2324

params.failoverReason === "format"

2425

) {

2526

return null;