fix: explain safe WorkdirDelete recovery in tool output

This commit is contained in:
2026-10-09 16:41:43 +09:00
parent c889c9a68d
commit ef642b34be
2 changed files with 123 additions and 2 deletions
@@ -734,8 +734,14 @@ impl WorkspaceHttpWorkdirBackend {
))?;
workdir_output(
format!(
"Workdir {workdir_id} removal disposition: {:?}",
response.disposition
"Workdir {workdir_id} removal disposition: {:?}. {}{}",
response.disposition,
removal_guidance(response.failure_category.as_deref()),
if response.retryable {
" After resolving the cause, repeat this WorkdirDelete with the same id and reason; current safety conditions will be rechecked."
} else {
""
}
),
&response,
)
@@ -920,6 +926,48 @@ fn decode_error(error: serde_json::Error) -> ToolError {
ToolError::ExecutionFailed(format!("decode Workspace Workdir response: {error}"))
}
fn removal_guidance(category: Option<&str>) -> &'static str {
match category {
None => "",
Some("mount_present") => {
"A mount remains inside the Workdir. Confirm its owner and usage, and have that owner safely release it. Do not recursively delete mounted data or implicitly unmount external or unknown resources."
}
Some("mount_check_unavailable") => "Restore Runtime mount inspection before deletion.",
Some("changes_present" | "dirty_or_unknown") => {
"Current changes are protected; preserve or resolve them before deletion."
}
Some("changes_unknown") => {
"Restore checkout inspection or preserve remaining content so current changes can be verified."
}
Some(
"ownership_unknown"
| "provider_identity_changed"
| "authority_changed"
| "authority_invalid",
) => {
"Confirm target identity and ownership with its owner; never remove a replacement or unknown resource."
}
Some("blocked_by_live_authority") => {
"Release current attachments, reservations and holds through their owning authority."
}
Some("permission_denied" | "resource_busy" | "storage_unavailable") => {
"Have the resource owner resolve access, usage or storage safely; do not force-clean or change shared resource permissions."
}
Some("runtime_unavailable" | "provider_unavailable") => {
"Restore the Runtime connection; the registry remains until deletion is confirmed."
}
Some("provider_observation_unknown" | "provider_cleanup_outcome_unknown") => {
"Deletion is not confirmed. Re-observe with normal removal after restoring provider access; the registry is retained."
}
Some("unsupported_target") => {
"Ask the owner for the provider's supported cleanup operation."
}
Some(_) => {
"Inspect correlated Runtime cleanup diagnostics with the owning operator and resolve the current cause."
}
}
}
fn workdir_output<T: Serialize>(summary: String, value: &T) -> Result<ToolOutput, ToolError> {
Ok(ToolOutput {
summary,
@@ -1218,6 +1266,45 @@ mod tests {
})
}
#[test]
fn ordinary_workdir_delete_reports_safe_cause_and_exact_retry_guidance() {
let client = Arc::new(RecordingWorkspaceClient::new(vec![response(json!({
"working_directory_id": "wd-retry",
"disposition": "attention_required",
"retryable": true,
"failure_category": "mount_present"
}))]));
let backend = WorkspaceHttpWorkdirBackend::new(client.clone());
let output = backend
.delete(WorkdirDeleteInput {
working_directory_id: "wd-retry".into(),
reason: "remove unused Workdir".into(),
})
.unwrap();
assert!(output.summary.contains("Confirm its owner"));
assert!(
output
.summary
.contains("Do not recursively delete mounted data")
);
assert!(output.summary.contains("same id and reason"));
assert!(output.summary.len() <= 600);
let content: serde_json::Value =
serde_json::from_str(output.content.as_deref().unwrap()).unwrap();
assert_eq!(content["failure_category"], "mount_present");
assert_eq!(content["retryable"], true);
let requests = client.requests();
assert_eq!(requests[0].method, WorkspaceRequestMethod::Delete);
assert_eq!(
serde_json::from_str::<serde_json::Value>(requests[0].body.as_deref().unwrap())
.unwrap()["reason"],
"remove unused Workdir"
);
let unknown = removal_guidance(Some("secret /private/provider/path"));
assert!(!unknown.contains("/private"));
assert!(!unknown.contains("secret"));
}
#[test]
fn workspace_request_failure_is_not_reported_as_session_unavailable() {
let error = workspace_workdir_error(WorkspaceClientError::Request(
@@ -0,0 +1,34 @@
# SubWorker / Workdir execution ergonomics during T-730
Observed during Workdir removal implementation in a dedicated clean checkout:
- Children using `profile: builtin:default` were given explicit writable scope
and command permission, but their available tools/context did not support the
requested implementation. They performed read-only investigation and reported
the missing capabilities. Recreating coding children with `profile: inherit`
exposed the needed implementation/Ticket tools. No rejected capability was
bypassed.
- Concurrent Workdir reads sometimes returned `filesystem provider busy`.
This is transient provider contention, not an authorization rejection. A
child initially treated it as a stop condition; a subsequent parent turn
clarified the difference and sequential retries proceeded normally.
- After a user interruption, repository changes and parent tasks remained, but
`WorkerList` no longer listed the previously active children. Their work was
not treated as approved or complete: the parent inspected concrete diffs,
reran Web/browser validation and delegated a fresh bounded Runtime follow-up.
Potential improvements:
1. Show effective child tool capabilities together with filesystem/command
grants before spawn, so a writable scope is not mistaken for an available
implementation tool surface.
2. Distinguish retryable provider contention from capability rejection in typed
error metadata and recommended recovery text.
3. Make interruption disposition and retained child-session observation grants
explicit. Preserved files are useful evidence, but they do not replace a
committed final child report or verification of tests.
These observations concern development-tool ergonomics, not product removal
requirements or permission to operate on live Workdirs/mounts. The T-730 work
uses temporary fixtures and ordinary Git publication; no live cleanup, unmount,
registry edits or runtime deployment were performed.