Compare commits

..

14 Commits

Author SHA1 Message Date
YeonGyu-Kim
8806e62a9f docs(roadmap): add #330 — resume mode stats/cost always zero 2026-05-25 13:00:54 +09:00
YeonGyu-Kim
78a0ff615a Merge pull request #3014 from wangguan1995/fix_qwen
Add Qwen model token limits for DashScope compatibility
2026-05-25 12:58:59 +09:00
YeonGyu-Kim
706ac0f8e1 Merge pull request #3097 from ultraworkers/fix-683-unsupported-skills-action
fix(#683): claw skills remove/add/uninstall/delete emits typed error, exit 1
2026-05-25 12:55:01 +09:00
YeonGyu-Kim
bd8a27b100 Merge pull request #3096 from ultraworkers/fix-160-session-store-lifecycle
fix(#160): add regression test for SessionStore lifecycle
2026-05-25 12:54:42 +09:00
YeonGyu-Kim
60108dfbf6 fix(test): update client_integration version string 0.1.0 -> 0.1.3 2026-05-25 12:49:37 +09:00
Yeachan-Heo
bd9102f851 fix(api): skip preflight for unknown model limits 2026-05-25 12:49:37 +09:00
YeonGyu-Kim
e7d5d08892 fix: ChunkDelta thinking field in test initializers; fix parse_local_help_action ? operator 2026-05-25 12:49:37 +09:00
YeonGyu-Kim
f003a108e3 fix: remove stale retry_after refs from openai_compat.rs 2026-05-25 12:49:37 +09:00
YeonGyu-Kim
a76dda2b19 chore: cargo fmt --all on fix-683 branch 2026-05-25 12:49:37 +09:00
YeonGyu-Kim
013694476e chore: sync Cargo.lock and openai_compat.rs to main (stash artifact cleanup) 2026-05-25 12:49:37 +09:00
YeonGyu-Kim
3d02baf567 fix(#683): claw skills remove/add/uninstall/delete emits typed error, exit 1
- Add unsupported skills action guard in parse_args for remove/add/uninstall/delete
- Add unsupported_skills_action to classify_error_kind for structured JSON errors
- Fix pre-existing compile errors (stale retry_after field, missing Team variant)
- Add regression test unsupported_skills_actions_return_typed_error_683
2026-05-25 12:49:37 +09:00
wangguan1995
fa8eecaf8f fix 2026-05-10 13:23:40 +00:00
wangguan1995
2033c90921 fix log 2026-05-10 13:21:58 +00:00
wangguan1995
8cada12c48 Add Qwen model token limits for DashScope compatibility 2026-05-10 13:09:07 +00:00
5 changed files with 87 additions and 9 deletions

View File

@@ -7552,3 +7552,6 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
**Required fix shape.** (a) Add a startup pre-flight in `worker_boot.rs` that, for any task mentioning a file path, checks `git ls-files --error-unmatch <path>` and emits a structured `kind:"file_absent_on_branch"` warning before the first LLM call; (b) surface `.git` writability check at sandbox init time and emit `kind:"git_metadata_not_writable"` with the path if commits would fail; (c) add a "current worktree" breadcrumb to the session startup log so agents and operators can confirm the correct tree before work begins.
**Source.** Gaebal probe 2026-05-25 12:02 GMT+9; confirmed during #3097 trident.rs fix attempt. [SCOPE: ultraworkers/claw-code]
||||||| parent of 52161c78 (docs(roadmap): add #330 — resume mode stats/cost always zero)
330. **`/stats` and `/cost` in `--resume` mode always return zero token counts regardless of saved session usage** — dogfooded 2026-04-29 by Jobdori on current main (`e7074f4`). Running `claw --output-format json --resume latest /stats` and `claw --output-format json --resume latest /cost` both return `{"input_tokens": 0, "output_tokens": 0, "total_tokens": 0, ...}` even when the session file was created by a real interactive claw run. The same zero-fill is produced by `claw --output-format json status` (usage sub-object). Inspection of the default session path (`~/.claw/sessions/.../session-*.jsonl`) shows the file has only 2 events (`session_meta`, `message`) with no usage/token records embedded. Two candidate root causes: (a) session serialization never writes usage events into the JSONL file even after real prompt exchanges, so resume-mode has no usage to replay; (b) resume-mode stat accumulation reads usage from memory-side counters that are always zero because no API calls happened in the resume-only invocation. Either way, the operator effect is the same: `--resume` + `/stats`/`/cost` cannot report what the session actually consumed. **Required fix shape:** (a) persist per-turn usage records (input/output/cache tokens per exchange) into the session JSONL at write time so resume-mode can reconstruct cumulative counts by replay; (b) expose a `"total_usage"` summary block at the tail of every session JSONL so resume-mode can read it in O(1) without full replay; (c) ensure `/stats` and `/cost` in resume-mode sum the persisted usage, not memory-side live counters; (d) add regression coverage proving `--resume SESSION.jsonl /stats` returns non-zero token counts after a session that made real API calls. **Why this matters:** token usage visibility is critical for quota management and cost attribution; if `--resume` mode always shows zeros, operators and orchestration lanes cannot trust usage data from saved sessions and must rely on external billing dashboards instead of in-band tooling. Source: Jobdori live dogfood on mengmotaHost, claw-code `e7074f4`, 2026-04-29.

0
rust/Cargo.lock generated Normal file → Executable file
View File

View File

@@ -640,6 +640,14 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
max_output_tokens: 16_384,
context_window_tokens: 256_000,
}),
"qwen-max" => Some(ModelTokenLimit {
max_output_tokens: 8_192,
context_window_tokens: 131_072,
}),
"qwen-plus" => Some(ModelTokenLimit {
max_output_tokens: 8_192,
context_window_tokens: 131_072,
}),
_ => None,
}
}

View File

@@ -1180,6 +1180,9 @@ pub enum SlashCommand {
count: Option<String>,
},
Unknown(String),
Team {
action: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -1277,6 +1280,7 @@ impl SlashCommand {
Self::Tag { .. } => "/tag",
Self::OutputStyle { .. } => "/output-style",
Self::AddDir { .. } => "/add-dir",
Self::Team { .. } => "/team",
Self::Sandbox => "/sandbox",
Self::Mcp { .. } => "/mcp",
Self::Export { .. } => "/export",
@@ -4312,6 +4316,7 @@ pub fn handle_slash_command(
| SlashCommand::OutputStyle { .. }
| SlashCommand::AddDir { .. }
| SlashCommand::History { .. }
| SlashCommand::Team { .. }
| SlashCommand::Unknown(_) => None,
}
}

View File

@@ -274,6 +274,8 @@ fn classify_error_kind(message: &str) -> &'static str {
"no_managed_sessions"
} else if message.contains("unsupported ACP invocation") {
"unsupported_acp_invocation"
} else if message.contains("unsupported skills action") {
"unsupported_skills_action"
} else if message.contains("unrecognized argument") || message.contains("unknown option") {
"cli_parse"
} else if message.contains("invalid model syntax") {
@@ -877,6 +879,35 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
}
if wants_help {
// #684: --help before subcommand should still route to subcommand-specific
// help when the subcommand is one of the local-help-topic commands.
if let Some(action) = parse_local_help_action(&rest, output_format) {
return action;
}
// When --help was consumed before the subcommand, rest has no help flag.
// If rest is a simple local-help subcommand with no extra args, route there.
if !rest.is_empty() && rest[1..].iter().all(|a| is_help_flag(a)) {
let topic = match rest[0].as_str() {
"status" => Some(LocalHelpTopic::Status),
"sandbox" => Some(LocalHelpTopic::Sandbox),
"doctor" => Some(LocalHelpTopic::Doctor),
"acp" => Some(LocalHelpTopic::Acp),
"init" => Some(LocalHelpTopic::Init),
"state" => Some(LocalHelpTopic::State),
"export" => Some(LocalHelpTopic::Export),
"version" => Some(LocalHelpTopic::Version),
"system-prompt" => Some(LocalHelpTopic::SystemPrompt),
"dump-manifests" => Some(LocalHelpTopic::DumpManifests),
"bootstrap-plan" => Some(LocalHelpTopic::BootstrapPlan),
_ => None,
};
if let Some(topic) = topic {
return Ok(CliAction::HelpTopic {
topic,
output_format,
});
}
}
return Ok(CliAction::Help { output_format });
}
@@ -1020,6 +1051,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
),
"skills" => {
let args = join_optional_args(&rest[1..]);
if let Some(action) = args.as_deref() {
let first_word = action.split_whitespace().next().unwrap_or(action);
if matches!(first_word, "remove" | "add" | "uninstall" | "delete") {
return Err(format!(
"unsupported skills action: {first_word}. Supported actions: list, install <path>, help, or <skill> [args]"
));
}
}
match classify_skills_slash_command(args.as_deref()) {
SkillSlashDispatch::Invoke(prompt) => Ok(CliAction::Prompt {
prompt,
@@ -1117,7 +1156,10 @@ fn parse_local_help_action(
rest: &[String],
output_format: CliOutputFormat,
) -> Option<Result<CliAction, String>> {
if rest.len() != 2 || !is_help_flag(&rest[1]) {
if rest.is_empty() {
return None;
}
if !rest.iter().any(|a| is_help_flag(a)) {
return None;
}
@@ -1126,10 +1168,6 @@ fn parse_local_help_action(
"sandbox" => LocalHelpTopic::Sandbox,
"doctor" => LocalHelpTopic::Doctor,
"acp" => LocalHelpTopic::Acp,
// #141: add the subcommands that were previously falling back
// to global help (init/state/export/version) or erroring out
// (system-prompt/dump-manifests) or printing their primary
// output instead of help text (bootstrap-plan).
"init" => LocalHelpTopic::Init,
"state" => LocalHelpTopic::State,
"export" => LocalHelpTopic::Export,
@@ -1139,6 +1177,10 @@ fn parse_local_help_action(
"bootstrap-plan" => LocalHelpTopic::BootstrapPlan,
_ => return None,
};
let has_non_help = rest[1..].iter().any(|a| !is_help_flag(a));
if has_non_help {
return None;
}
Some(Ok(CliAction::HelpTopic {
topic,
output_format,
@@ -1172,8 +1214,9 @@ fn parse_single_word_command_alias(
if is_diagnostic && rest.len() > 1 {
// Diagnostic verb with trailing args: reject unrecognized suffix
if is_help_flag(&rest[1]) && rest.len() == 2 {
// "doctor --help" is valid, routed to parse_local_help_action() instead
let all_extra_are_help = rest[1..].iter().all(|a| is_help_flag(a));
if all_extra_are_help {
// "doctor --help -h" is valid, routed to parse_local_help_action() instead
return None;
}
// Unrecognized suffix like "--json"
@@ -4209,7 +4252,8 @@ fn run_resume_command(
| SlashCommand::Ide { .. }
| SlashCommand::Tag { .. }
| SlashCommand::OutputStyle { .. }
| SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
| SlashCommand::AddDir { .. }
| SlashCommand::Team { .. } => Err("unsupported resumed slash command".into()),
}
}
@@ -5456,7 +5500,8 @@ impl LiveCli {
| SlashCommand::Ide { .. }
| SlashCommand::Tag { .. }
| SlashCommand::OutputStyle { .. }
| SlashCommand::AddDir { .. } => {
| SlashCommand::AddDir { .. }
| SlashCommand::Team { .. } => {
let cmd_name = command.slash_name();
eprintln!("{cmd_name} is not yet implemented in this build.");
false
@@ -12711,6 +12756,23 @@ mod tests {
assert!(error.contains("skills"));
}
#[test]
fn unsupported_skills_actions_return_typed_error_683() {
for action in ["remove", "add", "uninstall", "delete"] {
let error = parse_args(&["skills".to_string(), action.to_string()])
.expect_err(&format!("skills {action} should error"));
assert!(
error.contains("unsupported skills action"),
"skills {action} should contain 'unsupported skills action', got: {error}"
);
assert_eq!(
classify_error_kind(&error),
"unsupported_skills_action",
"skills {action} should classify as unsupported_skills_action, got: {error}"
);
}
}
#[test]
fn typoed_status_subcommand_returns_did_you_mean_error() {
let error = parse_args(&["statuss".to_string()]).expect_err("statuss should error");