Skip to content

Commit 28d1285

Browse files
Milhouszhangclaude
andauthored
plan-06: turn 生命周期收敛 — 取消传播 + 后台任务收割 + 出站配额 (#503)
* feat(runtime): add current turn context to AppState * feat(daemon): bound turn interaction waits behind TurnScope * feat(runtime): owner-aware background task registry with real stop * feat(runtime): owned, stoppable background agents and shells * feat(daemon): single finish_turn path with scoped child harvest Interaction-timeout smoke coverage shipped as the late-resolve-rejection variant (daemon_rejects_resolve_after_turn_finished) plus the Task 2 TurnScope unit tests, per the plan's fallback; the mock SSE stream was not extended to emit AskUserQuestion tool_use. * feat(runtime): per-turn outbound budget and turn-id origin stamping * fix(desktop): remove double-layer resolver no-ops and silent frontend fallback Rust corbina suite green (112 passed incl. REGISTERED_TAURI_COMMANDS meta-test). Playwright E2E not run here (needs a built frontend + browsers); the change is behavior-preserving for the fake-daemon tests, which already exercised the error-propagation path since canInvokeTauri() is false in the browser context. * refactor(runtime): drop redundant cancel and dead BackgroundTaskManager::stop finish_turn shared the turn's CancelToken with its TurnScope, and TurnScope::finish already flips the token for every non-Complete reason, so the explicit handle.cancel.cancel() in finish_turn was dead duplication — removed, with cancellation ownership documented on scope. BackgroundTaskManager::stop had no production callers: real stops route through request_stop / stop_scoped_by_turn -> stop_one (which cancels the token and kills the process). The bare stop only flipped status, orphaning the worker/process — a footgun the owner-aware system replaced. Removed it and reworked the two unit tests that relied on it to exercise the real convergence paths (request_stop + complete -> Stopped; complete frees a slot). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8f59a40 commit 28d1285

26 files changed

Lines changed: 1488 additions & 1087 deletions

apps/puffer-desktop/src-tauri/src/backend.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,9 @@ impl BackendState {
294294
| "workflow_runs_list"
295295
| "workflow_run_show" => workflow_runtime_unavailable(method),
296296
"run_agent_turn" => self.run_agent_turn(events.clone(), params),
297-
"resolve_permission" | "resolve_user_question" => Ok(json!({})),
297+
"resolve_permission" | "resolve_user_question" => anyhow::bail!(
298+
"{method} requires the daemon turn lifecycle; the in-process backend does not host interactive turns"
299+
),
298300
"cancel_turn" => {
299301
let turn_id = string_param(&params, &["turnId", "turn_id"])?;
300302
if let Some(flag) = self.turns.lock().unwrap().get(&turn_id) {
@@ -2323,6 +2325,22 @@ mod tests {
23232325

23242326
static TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
23252327

2328+
#[test]
2329+
fn resolver_methods_error_instead_of_noop_success() {
2330+
let backend = BackendState::new();
2331+
for method in ["resolve_permission", "resolve_user_question"] {
2332+
let result = backend.handle(
2333+
EventEmitter::websocket_only(),
2334+
method,
2335+
serde_json::json!({
2336+
"turnId": "missing-turn",
2337+
"requestId": "missing-request"
2338+
}),
2339+
);
2340+
assert!(result.is_err(), "{method} must not silently succeed");
2341+
}
2342+
}
2343+
23262344
#[test]
23272345
fn generate_media_result_serializes_artifacts_array() {
23282346
let result = GenerateMediaResult {

apps/puffer-desktop/src-tauri/src/lib.rs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -434,21 +434,46 @@ fn run_agent_turn(
434434

435435
#[tauri::command]
436436
fn resolve_permission(
437-
_turn_id: String,
438-
_request_id: String,
439-
_action: String,
437+
app: AppHandle,
438+
state: State<'_, SharedBackend>,
439+
turn_id: String,
440+
request_id: String,
441+
action: String,
440442
) -> Result<(), String> {
441-
Ok(())
443+
backend_call(
444+
app,
445+
state,
446+
"resolve_permission",
447+
json!({
448+
"turnId": turn_id,
449+
"requestId": request_id,
450+
"action": action,
451+
}),
452+
)
453+
.map(|_| ())
442454
}
443455

444456
#[tauri::command]
445457
fn resolve_user_question(
446-
_turn_id: String,
447-
_request_id: String,
448-
_answers: Value,
449-
_annotations: Value,
458+
app: AppHandle,
459+
state: State<'_, SharedBackend>,
460+
turn_id: String,
461+
request_id: String,
462+
answers: Value,
463+
annotations: Value,
450464
) -> Result<(), String> {
451-
Ok(())
465+
backend_call(
466+
app,
467+
state,
468+
"resolve_user_question",
469+
json!({
470+
"turnId": turn_id,
471+
"requestId": request_id,
472+
"answers": answers,
473+
"annotations": annotations,
474+
}),
475+
)
476+
.map(|_| ())
452477
}
453478

454479
#[tauri::command]

0 commit comments

Comments
 (0)