Skip to content

Commit cb68e2d

Browse files
committed
fix: harden windows runtime recovery paths
1 parent 51b91cf commit cb68e2d

2 files changed

Lines changed: 68 additions & 15 deletions

File tree

apps/server/src/services/agent.rs

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const DEFAULT_PTY_ROWS: u16 = 30;
77
struct AgentLifecycleFallbackState {
88
emitted_tool_started: bool,
99
emitted_turn_completed: bool,
10+
claude_session_id: Option<String>,
1011
}
1112

1213
fn initial_pty_size(cols: Option<u16>, rows: Option<u16>) -> PtySize {
@@ -21,29 +22,40 @@ fn initial_pty_size(cols: Option<u16>, rows: Option<u16>) -> PtySize {
2122
fn fallback_agent_lifecycle_from_output(
2223
state: &mut AgentLifecycleFallbackState,
2324
text: &str,
24-
) -> Option<(&'static str, &'static str, &'static str)> {
25+
) -> Option<(&'static str, &'static str, String)> {
2526
if state.emitted_tool_started || text.trim().is_empty() {
2627
return None;
2728
}
2829
state.emitted_tool_started = true;
29-
Some((
30-
"tool_started",
31-
"AgentProcessOutput",
32-
r#"{"source":"agent_process_output"}"#,
33-
))
30+
let data = state
31+
.claude_session_id
32+
.as_deref()
33+
.map(|session_id| {
34+
json!({
35+
"source": "agent_process_output",
36+
"session_id": session_id,
37+
})
38+
})
39+
.unwrap_or_else(|| {
40+
json!({
41+
"source": "agent_process_output",
42+
})
43+
})
44+
.to_string();
45+
Some(("tool_started", "AgentProcessOutput", data))
3446
}
3547

3648
fn fallback_agent_lifecycle_from_exit(
3749
state: &mut AgentLifecycleFallbackState,
38-
) -> Option<(&'static str, &'static str, &'static str)> {
50+
) -> Option<(&'static str, &'static str, String)> {
3951
if state.emitted_turn_completed || !state.emitted_tool_started {
4052
return None;
4153
}
4254
state.emitted_turn_completed = true;
4355
Some((
4456
"turn_completed",
4557
"AgentProcessExit",
46-
r#"{"source":"agent_process_exit"}"#,
58+
r#"{"source":"agent_process_exit"}"#.to_string(),
4759
))
4860
}
4961

@@ -237,7 +249,10 @@ pub(crate) fn agent_start(
237249
let workspace_id_out = workspace_id.clone();
238250
let session_out = session_id.clone();
239251
let session_out_num = session_id_num;
240-
let lifecycle_fallback_state = Arc::new(Mutex::new(AgentLifecycleFallbackState::default()));
252+
let lifecycle_fallback_state = Arc::new(Mutex::new(AgentLifecycleFallbackState {
253+
claude_session_id: effective_claude_session_id.clone(),
254+
..Default::default()
255+
}));
241256
let app_handle = app.clone();
242257
let state_handle = app.clone();
243258
let lifecycle_fallback_state_out = lifecycle_fallback_state.clone();
@@ -262,7 +277,7 @@ pub(crate) fn agent_start(
262277
&session_out,
263278
kind,
264279
source_event,
265-
data,
280+
&data,
266281
);
267282
}
268283
}
@@ -298,7 +313,7 @@ pub(crate) fn agent_start(
298313
&session_id,
299314
kind,
300315
source_event,
301-
data,
316+
&data,
302317
);
303318
}
304319
}
@@ -451,7 +466,7 @@ mod tests {
451466
Some((
452467
"tool_started",
453468
"AgentProcessOutput",
454-
r#"{"source":"agent_process_output"}"#,
469+
r#"{"source":"agent_process_output"}"#.to_string(),
455470
)),
456471
);
457472
assert_eq!(
@@ -460,6 +475,24 @@ mod tests {
460475
);
461476
}
462477

478+
#[test]
479+
fn fallback_agent_lifecycle_carries_known_claude_session_id() {
480+
let mut state = AgentLifecycleFallbackState {
481+
claude_session_id: Some("claude-resume-known".to_string()),
482+
..Default::default()
483+
};
484+
485+
assert_eq!(
486+
fallback_agent_lifecycle_from_output(&mut state, "fixture-running\n"),
487+
Some((
488+
"tool_started",
489+
"AgentProcessOutput",
490+
r#"{"session_id":"claude-resume-known","source":"agent_process_output"}"#
491+
.to_string(),
492+
)),
493+
);
494+
}
495+
463496
#[test]
464497
fn fallback_agent_lifecycle_only_emits_completion_after_output_started() {
465498
let mut state = AgentLifecycleFallbackState::default();
@@ -471,7 +504,7 @@ mod tests {
471504
Some((
472505
"turn_completed",
473506
"AgentProcessExit",
474-
r#"{"source":"agent_process_exit"}"#,
507+
r#"{"source":"agent_process_exit"}"#.to_string(),
475508
)),
476509
);
477510
assert_eq!(fallback_agent_lifecycle_from_exit(&mut state), None);

apps/web/src/features/workspace/WorkspaceScreen.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ const REQUIRED_RUNTIME_COMMANDS = [
265265
id: RuntimeRequirementStatus["id"];
266266
command: string;
267267
}>;
268+
const ROUTE_RUNTIME_ATTACH_RECOVERY_DELAYS_MS = [0, 1_000, 3_000, 7_000] as const;
268269

269270
type RuntimeRequirementSpec = {
270271
id: RuntimeRequirementStatus["id"];
@@ -768,11 +769,15 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
768769
});
769770
}
770771
attachRouteRuntime();
771-
const timer = window.setTimeout(attachRouteRuntime, 1000);
772+
const timers = ROUTE_RUNTIME_ATTACH_RECOVERY_DELAYS_MS
773+
.filter((delayMs) => delayMs > 0)
774+
.map((delayMs) => window.setTimeout(attachRouteRuntime, delayMs));
772775
void ensureWorkspaceTerminal(routeWorkspaceId);
773776
return () => {
774777
cancelled = true;
775-
window.clearTimeout(timer);
778+
timers.forEach((timer) => {
779+
window.clearTimeout(timer);
780+
});
776781
};
777782
}
778783

@@ -796,6 +801,21 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
796801
return;
797802
}
798803
if (!runtimeSnapshot) {
804+
const snapshot = await withServiceFallback(() => getWorkspaceSnapshot(routeWorkspaceId), null);
805+
if (cancelled || !isWorkspaceSyncVersionCurrent(routeWorkspaceId, syncVersion)) {
806+
return;
807+
}
808+
if (snapshot) {
809+
updateState((current) => upsertWorkspaceSnapshot(
810+
current,
811+
snapshot,
812+
locale,
813+
appSettings,
814+
uiState,
815+
));
816+
void ensureWorkspaceTerminal(routeWorkspaceId);
817+
return;
818+
}
799819
navigate("/workspace", { replace: true });
800820
return;
801821
}

0 commit comments

Comments
 (0)