Skip to content

Commit 2684737

Browse files
committed
fix(#776): resume command errors now return typed error_kind + non-null hint (invalid_history_count, session action errors)
1 parent 028998d commit 2684737

2 files changed

Lines changed: 24 additions & 12 deletions

File tree

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7717,3 +7717,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
77177717
774. **`claw agents bogus`, `claw plugins bogus`, `claw mcp bogus` returned `hint: null`** — dogfooded 2026-05-27 on `727a1ea4`. Three "unknown subcommand" envelopes had `error_kind` correctly set but `hint: null`: (1) `unknown_agents_subcommand` — both text and JSON handler emitted single-line error with inline remediation after `.`, no `\n`; (2) `unknown_plugins_action` — same, period-delimited remediation; (3) `unknown_mcp_action` — `render_mcp_usage_json` never included a `hint` field at all. Fixes: (1)+(2) added `\n` before remediation suffix in `commands/src/lib.rs`; (3) added `hint` field to `render_mcp_usage_json` pointing at supported actions. All three now return non-null `hint`. 36 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori envelope-consistency probe on `727a1ea4`, 2026-05-27.
77187718

77197719
775. **Missing integration tests for #769-#771 interactive-only guards and #774 hint fields** — dogfooded 2026-05-27 on `c760a49c`. Fixes #769-#771 (session/cost/clear/memory/ultraplan/model/usage/stats/fork interactive-only guards) and #774 (agents/plugins/mcp unknown-subcommand hints) had no integration tests — a regression in any of those 10+ match arms would go undetected. Also: classify_error_kind unit test for `unknown_agents_subcommand` used the old single-line format string, not the `\n`-delimited format emitted after #774. Fixed: (1) updated unit test string to match new `\n`-delimited emission; (2) added `agents_plugins_mcp_unknown_subcommand_have_hint_774` asserting `error_kind` + non-null `hint` for all three; (3) added `interactive_only_guard_batch_769_to_771` asserting `interactive_only` + non-null `hint` for 10 cases. 38 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori test-coverage sweep on `c760a49c`, 2026-05-27.
7720+
7721+
776. **Resume-mode JSON errors had opaque `error_kind:"resume_command_error"` + `hint:null`** — dogfooded 2026-05-27 on `028998d0` (pinpoint identified by Gaebal-gajae). `run_resume_command` returned errors (e.g. from `parse_history_count`) with hardcoded `error_kind:"resume_command_error"` and the full error string in `error` with no hint extraction. Wrappers had to regex prose instead of switching on typed fields. Three co-located gaps fixed: (1) `resume_session` JSON error path now applies `classify_error_kind` + `split_error_hint` so errors get specific `error_kind` (e.g. `invalid_history_count`) and non-null `hint`; (2) `parse_history_count` errors now use `invalid_history_count:` prefix + `\n` usage hint; (3) `/session exists|delete|switch|fork` missing-arg and unsupported-action errors now use `\n`-delimited format with `unsupported_resumed_command:` prefix. Existing test updated to match new error message format. 38 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `028998d0`, 2026-05-27.

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ fn classify_error_kind(message: &str) -> &'static str {
323323
"unsupported_config_section"
324324
} else if message.contains("unknown_plugins_action") {
325325
"unknown_plugins_action"
326+
} else if message.starts_with("invalid_history_count:") || message.contains("invalid count") {
327+
"invalid_history_count"
326328
} else if message.starts_with("missing_prompt:") {
327329
"missing_prompt"
328330
} else if message.contains("has been removed.") {
@@ -3370,14 +3372,20 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
33703372
}
33713373
Err(error) => {
33723374
if output_format == CliOutputFormat::Json {
3375+
// #776: classify + split so wrappers get typed fields instead of
3376+
// hardcoded "resume_command_error" + prose in the error field
3377+
let full_error = error.to_string();
3378+
let error_kind = classify_error_kind(&full_error);
3379+
let (short_reason, hint) = split_error_hint(&full_error);
33733380
eprintln!(
33743381
"{}",
33753382
serde_json::json!({
3376-
"kind": "resume_command_error",
3383+
"kind": error_kind,
33773384
"action": "resume",
33783385
"status": "error",
3379-
"error_kind": "resume_command_error",
3380-
"error": error.to_string(),
3386+
"error_kind": error_kind,
3387+
"error": short_reason,
3388+
"hint": hint,
33813389
"exit_code": 2,
33823390
"command": raw_command,
33833391
})
@@ -6847,7 +6855,7 @@ fn run_resumed_session_command(
68476855
}
68486856
Some("exists") => {
68496857
let Some(target) = target else {
6850-
return Err("/session exists requires a session id".into());
6858+
return Err("/session exists requires a session id.\nUsage: claw --resume <session> /session exists <session-id>".into());
68516859
};
68526860
let value = session_exists_json(target, &session.session_id)?;
68536861
let exists = value
@@ -6866,7 +6874,7 @@ fn run_resumed_session_command(
68666874
}
68676875
Some("delete") => {
68686876
let Some(target) = target else {
6869-
return Err("/session delete requires a session id".into());
6877+
return Err("/session delete requires a session id.\nUsage: claw --resume <session> /session delete <session-id> --force".into());
68706878
};
68716879
Ok(ResumeCommandOutcome {
68726880
session: session.clone(),
@@ -6883,7 +6891,7 @@ fn run_resumed_session_command(
68836891
}
68846892
Some("delete-force") => {
68856893
let Some(target) = target else {
6886-
return Err("/session delete requires a session id".into());
6894+
return Err("/session delete requires a session id.\nUsage: claw --resume <session> /session delete <session-id> --force".into());
68876895
};
68886896
let handle = resolve_session_reference(target)?;
68896897
if handle.id == session.session_id || handle.path == session_path {
@@ -6911,8 +6919,8 @@ fn run_resumed_session_command(
69116919
})),
69126920
})
69136921
}
6914-
Some("switch" | "fork") => Err("unsupported resumed slash command".into()),
6915-
Some(other) => Err(format!("unsupported resumed /session action: {other}").into()),
6922+
Some("switch" | "fork") => Err("unsupported_resumed_command: /session switch and /session fork require an interactive REPL.\nUsage: claw (then /session switch <id>) or claw --resume <session>".into()),
6923+
Some(other) => Err(format!("unsupported_resumed_command: /session {other} is not supported in resume mode.\nSupported: list, exists, delete").into()),
69166924
}
69176925
}
69186926

@@ -8413,11 +8421,12 @@ fn parse_history_count(raw: Option<&str>) -> Result<usize, String> {
84138421
let Some(raw) = raw else {
84148422
return Ok(DEFAULT_HISTORY_LIMIT);
84158423
};
8424+
// #776: use \n-delimited format so split_error_hint extracts hint into JSON envelopes
84168425
let parsed: usize = raw
84178426
.parse()
8418-
.map_err(|_| format!("history: invalid count '{raw}'. Expected a positive integer."))?;
8427+
.map_err(|_| format!("invalid_history_count: '{raw}' is not a positive integer.\nUsage: /history [count] (default: {DEFAULT_HISTORY_LIMIT})"))?;
84198428
if parsed == 0 {
8420-
return Err("history: count must be greater than 0.".to_string());
8429+
return Err(format!("invalid_history_count: count must be greater than 0.\nUsage: /history [count] (default: {DEFAULT_HISTORY_LIMIT})"));
84218430
}
84228431
Ok(parsed)
84238432
}
@@ -15148,8 +15157,9 @@ UU conflicted.rs",
1514815157
let parsed = parse_history_count(raw);
1514915158

1515015159
// then
15151-
assert!(parsed.is_err());
15152-
assert!(parsed.unwrap_err().contains("invalid count 'abc'"));
15160+
// #776: updated to match new invalid_history_count: prefix format
15161+
let err = parsed.expect_err("non-numeric count should fail");
15162+
assert!(err.contains("invalid_history_count:") && err.contains("'abc'"));
1515315163
}
1515415164

1515515165
#[test]

0 commit comments

Comments
 (0)