Skip to content

Commit 2bab408

Browse files
committed
Keep resumed /status JSON aligned with live status output
The resumed slash-command path built a reduced status JSON payload by hand, so it drifted from the fresh status schema and dropped metadata like model, permission mode, workspace counters, and sandbox details. Reuse a shared status JSON builder for both code paths and tighten the resume regression tests to lock parity in place. Constraint: Resume mode does not carry an active runtime model, so restored sessions continue to report the existing restored-session sentinel value Rejected: Copy the fresh status JSON shape into the resume path again | would recreate the same schema drift risk Confidence: high Scope-risk: narrow Directive: Keep resumed and fresh /status JSON on the same helper so future schema changes stay in parity Tested: Reproduced failure in temporary HEAD worktree with strengthened resumed_status_command_emits_structured_json_when_requested Tested: cargo test -p rusty-claude-cli resumed_status_command_emits_structured_json_when_requested --test resume_slash_commands -- --exact --nocapture Tested: cargo test -p rusty-claude-cli doctor_and_resume_status_emit_json_when_requested --test output_format_contract -- --exact --nocapture Tested: cargo test --workspace Tested: cargo fmt --check Tested: cargo clippy --workspace --all-targets -- -D warnings
1 parent f7321ca commit 2bab408

File tree

3 files changed

+81
-62
lines changed

3 files changed

+81
-62
lines changed

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

Lines changed: 69 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,24 +1570,19 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
15701570
&& matches!(command, SlashCommand::Status)
15711571
{
15721572
let tracker = UsageTracker::from_session(&session);
1573-
let usage = tracker.cumulative_usage();
15741573
let context = status_context(Some(&resolved_path)).expect("status context");
1575-
let value = json!({
1576-
"kind": "status",
1577-
"messages": session.messages.len(),
1578-
"turns": tracker.turns(),
1579-
"latest_total": tracker.current_turn_usage().total_tokens(),
1580-
"cumulative_input": usage.input_tokens,
1581-
"cumulative_output": usage.output_tokens,
1582-
"cumulative_total": usage.total_tokens(),
1583-
"workspace": {
1584-
"cwd": context.cwd,
1585-
"project_root": context.project_root,
1586-
"git_branch": context.git_branch,
1587-
"git_state": context.git_summary.headline(),
1588-
"session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()),
1589-
}
1590-
});
1574+
let value = status_json_value(
1575+
"restored-session",
1576+
StatusUsage {
1577+
message_count: session.messages.len(),
1578+
turns: tracker.turns(),
1579+
latest: tracker.current_turn_usage(),
1580+
cumulative: tracker.cumulative_usage(),
1581+
estimated_tokens: 0,
1582+
},
1583+
default_permission_mode().as_str(),
1584+
&context,
1585+
);
15911586
println!(
15921587
"{}",
15931588
serde_json::to_string_pretty(&value).expect("status json")
@@ -3845,54 +3840,68 @@ fn print_status_snapshot(
38453840
),
38463841
CliOutputFormat::Json => println!(
38473842
"{}",
3848-
serde_json::to_string_pretty(&json!({
3849-
"kind": "status",
3850-
"model": model,
3851-
"permission_mode": permission_mode.as_str(),
3852-
"usage": {
3853-
"messages": usage.message_count,
3854-
"turns": usage.turns,
3855-
"latest_total": usage.latest.total_tokens(),
3856-
"cumulative_input": usage.cumulative.input_tokens,
3857-
"cumulative_output": usage.cumulative.output_tokens,
3858-
"cumulative_total": usage.cumulative.total_tokens(),
3859-
"estimated_tokens": usage.estimated_tokens,
3860-
},
3861-
"workspace": {
3862-
"cwd": context.cwd,
3863-
"project_root": context.project_root,
3864-
"git_branch": context.git_branch,
3865-
"git_state": context.git_summary.headline(),
3866-
"changed_files": context.git_summary.changed_files,
3867-
"staged_files": context.git_summary.staged_files,
3868-
"unstaged_files": context.git_summary.unstaged_files,
3869-
"untracked_files": context.git_summary.untracked_files,
3870-
"session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()),
3871-
"loaded_config_files": context.loaded_config_files,
3872-
"discovered_config_files": context.discovered_config_files,
3873-
"memory_file_count": context.memory_file_count,
3874-
},
3875-
"sandbox": {
3876-
"enabled": context.sandbox_status.enabled,
3877-
"active": context.sandbox_status.active,
3878-
"supported": context.sandbox_status.supported,
3879-
"in_container": context.sandbox_status.in_container,
3880-
"requested_namespace": context.sandbox_status.requested.namespace_restrictions,
3881-
"active_namespace": context.sandbox_status.namespace_active,
3882-
"requested_network": context.sandbox_status.requested.network_isolation,
3883-
"active_network": context.sandbox_status.network_active,
3884-
"filesystem_mode": context.sandbox_status.filesystem_mode.as_str(),
3885-
"filesystem_active": context.sandbox_status.filesystem_active,
3886-
"allowed_mounts": context.sandbox_status.allowed_mounts,
3887-
"markers": context.sandbox_status.container_markers,
3888-
"fallback_reason": context.sandbox_status.fallback_reason,
3889-
}
3890-
}))?
3843+
serde_json::to_string_pretty(&status_json_value(
3844+
model,
3845+
usage,
3846+
permission_mode.as_str(),
3847+
&context,
3848+
))?
38913849
),
38923850
}
38933851
Ok(())
38943852
}
38953853

3854+
fn status_json_value(
3855+
model: &str,
3856+
usage: StatusUsage,
3857+
permission_mode: &str,
3858+
context: &StatusContext,
3859+
) -> serde_json::Value {
3860+
json!({
3861+
"kind": "status",
3862+
"model": model,
3863+
"permission_mode": permission_mode,
3864+
"usage": {
3865+
"messages": usage.message_count,
3866+
"turns": usage.turns,
3867+
"latest_total": usage.latest.total_tokens(),
3868+
"cumulative_input": usage.cumulative.input_tokens,
3869+
"cumulative_output": usage.cumulative.output_tokens,
3870+
"cumulative_total": usage.cumulative.total_tokens(),
3871+
"estimated_tokens": usage.estimated_tokens,
3872+
},
3873+
"workspace": {
3874+
"cwd": context.cwd,
3875+
"project_root": context.project_root,
3876+
"git_branch": context.git_branch,
3877+
"git_state": context.git_summary.headline(),
3878+
"changed_files": context.git_summary.changed_files,
3879+
"staged_files": context.git_summary.staged_files,
3880+
"unstaged_files": context.git_summary.unstaged_files,
3881+
"untracked_files": context.git_summary.untracked_files,
3882+
"session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()),
3883+
"loaded_config_files": context.loaded_config_files,
3884+
"discovered_config_files": context.discovered_config_files,
3885+
"memory_file_count": context.memory_file_count,
3886+
},
3887+
"sandbox": {
3888+
"enabled": context.sandbox_status.enabled,
3889+
"active": context.sandbox_status.active,
3890+
"supported": context.sandbox_status.supported,
3891+
"in_container": context.sandbox_status.in_container,
3892+
"requested_namespace": context.sandbox_status.requested.namespace_restrictions,
3893+
"active_namespace": context.sandbox_status.namespace_active,
3894+
"requested_network": context.sandbox_status.requested.network_isolation,
3895+
"active_network": context.sandbox_status.network_active,
3896+
"filesystem_mode": context.sandbox_status.filesystem_mode.as_str(),
3897+
"filesystem_active": context.sandbox_status.filesystem_active,
3898+
"allowed_mounts": context.sandbox_status.allowed_mounts,
3899+
"markers": context.sandbox_status.container_markers,
3900+
"fallback_reason": context.sandbox_status.fallback_reason,
3901+
}
3902+
})
3903+
}
3904+
38963905
fn status_context(
38973906
session_path: Option<&Path>,
38983907
) -> Result<StatusContext, Box<dyn std::error::Error>> {

rust/crates/rusty-claude-cli/tests/output_format_contract.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,10 @@ fn doctor_and_resume_status_emit_json_when_requested() {
130130
],
131131
);
132132
assert_eq!(resumed["kind"], "status");
133-
assert_eq!(resumed["messages"], 1);
133+
assert_eq!(resumed["model"], "restored-session");
134+
assert_eq!(resumed["usage"]["messages"], 1);
135+
assert!(resumed["workspace"]["cwd"].as_str().is_some());
136+
assert!(resumed["sandbox"]["filesystem_mode"].as_str().is_some());
134137
}
135138

136139
fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value {

rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,18 @@ fn resumed_status_command_emits_structured_json_when_requested() {
261261
let parsed: Value =
262262
serde_json::from_str(stdout.trim()).expect("resume status output should be json");
263263
assert_eq!(parsed["kind"], "status");
264-
assert_eq!(parsed["messages"], 1);
264+
assert_eq!(parsed["model"], "restored-session");
265+
assert_eq!(parsed["permission_mode"], "danger-full-access");
266+
assert_eq!(parsed["usage"]["messages"], 1);
267+
assert!(parsed["usage"]["turns"].is_number());
268+
assert!(parsed["workspace"]["cwd"].as_str().is_some());
265269
assert_eq!(
266270
parsed["workspace"]["session"],
267271
session_path.to_str().expect("utf8 path")
268272
);
273+
assert!(parsed["workspace"]["changed_files"].is_number());
274+
assert_eq!(parsed["workspace"]["loaded_config_files"].as_u64(), Some(0));
275+
assert!(parsed["sandbox"]["filesystem_mode"].as_str().is_some());
269276
}
270277

271278
fn run_claw(current_dir: &Path, args: &[&str]) -> Output {

0 commit comments

Comments
 (0)