Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ export function isTransientCliExitCode1(message: string | undefined): boolean {
// writes the same user turn into the rollout JSONL again (see bug-report
// 2026-04-19-codex-transient-retry-context-overflow).
if (/ran out of room|context window|context_window/i.test(message)) return false;
// clowder-ai#1324 (refs #848): the CLI's argument parser rejected our argv. The retry
// re-runs the *identical* argv, so it is rejected identically — the only thing a retry
// buys is a second failure (that is literally the "×2" users saw on the error bubble).
// formatCliExitError appends the classified reasonCode to the message, so the tag is the
// reliable channel here; the humanized text is i18n-mutable and must not be matched on.
if (/\[incompatible_cli_arguments\]/.test(message)) return false;
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export const TRIGGERING_REASON_CODES = new Set([
'tool_call_parse_failed',
'spawn_failed',
'invalid_config',
// clowder-ai#1324/#1325: the harness's managed argv drifted from the installed CLI
// version. Unlike upstream_policy_reject (an upstream decision we should not file
// against ourselves), this IS our bug — auto-filing is the point. @codex-terra P2:
// shouldTrigger()'s final gate is this allowlist, NOT the EXCLUDED denylist above,
// so merely staying out of the denylist silently produced the opposite behaviour.
'incompatible_cli_arguments',
]);

/** Transient/internal codes that should NOT trigger (not user-actionable). */
Expand Down
8 changes: 8 additions & 0 deletions packages/api/src/utils/cli-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ const REASON_TEXT: Record<CliErrorReasonCode, { summary: string; hint: string }>
// policy engine (Codex 0.98+) rejected content ("flagged for possible cybersecurity risk").
// NOT a Clowder AI bug — the upstream policy layer made the call. User needs actionable
// rephrase guidance, not "unknown CLI error" fallback. Excluded from F222 auto-issue.
// clowder-ai#1324 (refs #848): harness argv vs installed CLI version drifted apart.
// Plain text only — CliDiagnosticsPanel renders publicHint verbatim in a <span>.
// Deliberately says "重试也不会好" because the retry is now suppressed: users who saw
// the old "未识别的 CLI 错误 ×2" were watching a deterministic failure counted twice.
incompatible_cli_arguments: {
summary: 'CLI 参数与当前 CLI 版本不兼容',
hint: 'CLI 自身的参数解析器拒绝了这次调用——通常是 CLI 升级后某个 flag 被移除、改名,或变成了与其他 flag 互斥,和猫咖的调用方式产生了版本漂移。这不是你的配置或额度问题,重试也不会好(同样的参数会被同样拒绝,系统已跳过无谓重试)。展开下方“详细诊断”能看到 CLI 原文点名的具体参数;把它连同 CLI 版本(例如 kimi --version / codex --version)反馈给维护者即可修复。',
},
upstream_policy_reject: {
summary: '上游 provider policy 拒绝',
hint: 'CLI 上游 provider 的 policy 引擎拒绝了这次请求(例如判断为敏感内容)。这不是 Clowder AI bug — 是 provider 侧决策。建议:换个表达方式重试 prompt(provider 通常给了 "try rephrasing" 提示);换一只不同 provider 的猫;反复触发去查你用的 provider policy 文档(Anthropic / OpenAI / DeepSeek 各有 acceptable use policy)。展开下方"详细诊断"看具体 policy 消息。',
Expand Down
31 changes: 31 additions & 0 deletions packages/api/src/utils/cli-error-patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,37 @@ export const CLASSIFIER_PATTERNS: Array<{ code: CliErrorReasonCode; regex: RegEx
code: 'upstream_policy_reject',
regex: /flagged for possible cybersecurity risk/i,
},
// clowder-ai#1324 (refs #848 "CLI upgrade with format incompatibility"): the CLI's own
// argument parser rejected the argv the harness built. Deterministic — the same argv is
// rejected identically forever, so this also disables the transient retry (invoke-helpers).
//
// LL-059 discipline — both regexes are transcribed from logged witnesses only, no invented
// parser variants. Runtime log survey 2026-08-06..08-10 (108 unknown-CLI-error events total):
// 48x `error: unknown option '--agent-file'` — kimi-code predating the flag
// 28x `error: Cannot combine --agent/--agent-file with --session/--continue: ...`
// — kimi-code >=0.30 added the validation
// Same disease, two CLI versions. Deliberately NOT generalized to clap/argparse phrasings
// (`unexpected argument`, `unrecognized arguments`) — no witness in our logs yet; add them
// when one shows up, not before.
//
// @codex-terra P2 on PR #1325: the first cut matched ANY rejected flag. But F127
// cliConfigArgs lets an operator pass their own flags, so a typo like
// `--definitely-not-a-real-flag` would have been blamed on the harness — telling the
// user "this is not your config" (false) and, once the F222 allowlist entry below
// lands, auto-filing an issue against ourselves for someone's typo.
//
// This reasonCode means specifically: MANAGED argv (flags the harness itself builds)
// drifted from the installed CLI version. So the pattern is anchored to the managed
// system-prompt flags, not to flag-rejection in general. A generic user-flag rejection
// needs its own neutral reasonCode/hint — deliberately not invented here without a
// witness (LL-059), and called out in the PR body as an explicit gap.
//
// Both phrases are highly specific, so ordering is not load-bearing; kept above the broad
// AC-A4 keyword block for provenance clarity.
{
code: 'incompatible_cli_arguments',
regex: /(?:unknown option|Cannot combine)\s+['"`]?--agent(?:-file)?\b/i,
},
// New 7 (AC-A4) — ordered most-specific first to avoid mis-classification
{
code: 'model_not_found',
Expand Down
49 changes: 49 additions & 0 deletions packages/api/test/cli-diagnostics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,55 @@ test('AC-A5: unknown stderr → sanitized safeExcerpt (#857), publicSummary fall
assert.ok(d.publicHint.length > 0);
});

// clowder-ai#1324 / #848: the harness's argv and the installed CLI version drift apart.
// Two witnessed shapes from the SAME disease, 76 events over 2026-08-06..08-10:
// 48x error: unknown option '--agent-file' (kimi-code lacked the flag)
// 28x error: Cannot combine --agent/--agent-file with --session/--continue
// (kimi-code >=0.30 added validation)
// Both fell through to "未识别的 CLI 错误" + a pointless transient retry. LL-059 discipline:
// these two regexes come from logged witnesses only — no invented parser variants.
test('#1324: CLI argv/version incompatibility classifies instead of falling through to unknown', () => {
const unknownOption = "error: unknown option '--agent-file'";
const cannotCombine =
'error: Cannot combine --agent/--agent-file with --session/--continue: the agent is bound at ' +
'session creation and the bound agent is restored automatically on resume.';

for (const raw of [unknownOption, cannotCombine]) {
const d = buildCliDiagnostics({ rawText: raw, debugRef: baseRef });
assert.strictEqual(d.reasonCode, 'incompatible_cli_arguments', `must classify: ${raw.slice(0, 40)}`);
assert.doesNotMatch(d.publicSummary, /未识别/, 'a diagnosable argv error must not read as "unknown"');
// KD-1: reasonCode is defined, so the excerpt is admitted through the existing whitelist.
assert.ok(d.safeExcerpt, 'classified reasonCode admits safeExcerpt');
assert.strictEqual(d.excerptSource, 'classifier');
}

// AC-A9 red line: raw stderr must never reach publicSummary/publicHint verbatim.
const d = buildCliDiagnostics({ rawText: cannotCombine, debugRef: baseRef });
assert.doesNotMatch(d.publicSummary, /--agent-file/, 'no raw argv text in publicSummary');
assert.doesNotMatch(d.publicHint, /--agent-file/, 'no raw argv text in publicHint');
});

test('#1324: ordinary unknown stderr must still fall through (no over-matching)', () => {
// Guard against the classifier swallowing unrelated errors just because they contain a dash.
const d = buildCliDiagnostics({ rawText: 'some weird thing happened with --verbose enabled', debugRef: baseRef });
assert.strictEqual(d.reasonCode, undefined, 'a mere flag mention is not an argv rejection');
});

// @codex-terra P2 on PR #1325: F127 cliConfigArgs lets an operator pass their own flags.
// A rejected *user* flag is not a harness/CLI version drift — blaming it on the harness
// tells the user "this is not your config" (false) and auto-files an issue for their typo.
test('#1325: a rejected USER flag must NOT be attributed to harness argv drift', () => {
const d = buildCliDiagnostics({
rawText: "error: unknown option '--definitely-not-a-real-kimi-flag'",
debugRef: baseRef,
});
assert.notStrictEqual(
d.reasonCode,
'incompatible_cli_arguments',
'only MANAGED argv (harness-built flags) counts as version drift',
);
});

test('AC-A1 + AC-A5: known reasonCode → safeExcerpt filled, publicSummary/Hint reasonable', () => {
const d = buildCliDiagnostics({
rawText:
Expand Down
25 changes: 25 additions & 0 deletions packages/api/test/invoke-single-cat.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3625,6 +3625,31 @@ describe('invokeSingleCat audit events (P1 fix)', () => {
);
});

it('isTransientCliExitCode1: argv/CLI-version incompatibility must NOT be retried (deterministic)', async () => {
const { isTransientCliExitCode1 } = await import(
'../dist/domains/cats/services/agents/invocation/invoke-helpers.js'
);

// Real shape after formatCliExitError appends the classified reasonCode.
// Witnessed 76x in runtime logs 2026-08-06..08-10 (48x "unknown option
// '--agent-file'" + 28x "Cannot combine --agent/--agent-file with
// --session/--continue") — every one of them retried once for nothing,
// which is where the user-visible "未识别的 CLI 错误 ×2" came from.
const argvMsg = 'Kimi CLI: CLI 异常退出 (code: 1, signal: none) [incompatible_cli_arguments]';
assert.equal(
isTransientCliExitCode1(argvMsg),
false,
'a CLI that rejected our argv rejects the identical argv again — retrying only doubles the failure',
);

// Regression guard: vanilla transient exit still retries
assert.equal(
isTransientCliExitCode1('Kimi CLI: CLI 异常退出 (code: 1, signal: none)'),
true,
'untagged transient exit must stay retryable',
);
});

it('session self-heal: retries once without --resume when Claude reports missing conversation', async () => {
let invokeCount = 0;
const sessionDeletes = [];
Expand Down
19 changes: 19 additions & 0 deletions packages/api/test/services/frustration-detector.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ describe('F222: shouldTrigger — CLI error', () => {
);
});

// clowder-ai#1325 (@codex-terra P2): shouldTrigger's final gate is the TRIGGERING
// allowlist, not the EXCLUDED denylist — so "deliberately left out of EXCLUDED"
// silently produced the OPPOSITE of the intended auto-file behaviour. An argv/CLI
// version drift is our own bug, so it must auto-file; this assertion is what makes
// that claim checkable instead of asserted in a PR body.
it('triggers on incompatible_cli_arguments (harness argv drift is OUR bug → auto-file)', () => {
assert.equal(
shouldTrigger({
type: 'cli_error',
diagnostics: {
reasonCode: 'incompatible_cli_arguments',
publicSummary: 'CLI 参数与当前 CLI 版本不兼容',
publicHint: 'hint',
},
}),
true,
);
});

it('does NOT trigger on server_overloaded (transient)', () => {
assert.equal(
shouldTrigger({
Expand Down
9 changes: 8 additions & 1 deletion packages/shared/src/types/cli-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ export type CliErrorReasonCode =
* followed by `turn.failed` + exit 1. NOT a Clowder AI bug — upstream policy engine decision.
* Users get humanized rephrase guidance instead of generic "unknown CLI error". Excluded
* from F222 FrustrationDetector auto-issue triggering (not user-actionable within our scope). */
| 'upstream_policy_reject';
| 'upstream_policy_reject'
/** clowder-ai#1324 (refs #848): the CLI's own argument parser rejected the argv the
* harness built — the installed CLI version and our call shape have drifted apart
* (flag removed / renamed / newly mutually-exclusive). Deterministic by construction:
* the same argv is rejected identically every time, so this reasonCode also switches
* OFF the transient-exit retry in invoke-helpers.isTransientCliExitCode1. Distinct from
* invalid_config (a config *file* is malformed) and spawn_failed (binary missing). */
| 'incompatible_cli_arguments';

/**
* Structured CLI error payload (Phase A KD-1 white-list admission).
Expand Down
6 changes: 6 additions & 0 deletions packages/web/src/components/CliDiagnosticsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ const REASON_PALETTE: Record<CliErrorReasonCode, Palette> = {
// addressing a decoder-drift or context-window issue. Shield-X icon signals
// "gated at upstream boundary" — visually distinct from key (auth) / gauge (quota).
upstream_policy_reject: { ...PALETTE_COGNITIVE, Icon: ShieldXIcon },
// clowder-ai#1324/#1325 (@codex-terra review): the harness's MANAGED argv drifted from the
// installed CLI version (flag removed / renamed / newly mutually exclusive). System tier
// (slate) on purpose — deliberately NOT PALETTE_USER_FIX: nothing in the user's config or
// credentials is wrong, so painting it as user-fixable would contradict the hint, which
// says exactly the opposite. Wrench = broken machinery on our side, awaiting a harness fix.
incompatible_cli_arguments: { ...PALETTE_SYSTEM, Icon: WrenchIcon },
};

const UNKNOWN_PALETTE: Palette = { ...PALETTE_SYSTEM, Icon: UnknownReasonIcon };
Expand Down
Loading