Skip to content

fix(tui): recover runtime status on healthy heartbeats - #132

Open
danielkov wants to merge 1 commit into
mainfrom
fix/runtime-heartbeat-recovery
Open

fix(tui): recover runtime status on healthy heartbeats#132
danielkov wants to merge 1 commit into
mainfrom
fix/runtime-heartbeat-recovery

Conversation

@danielkov

Copy link
Copy Markdown
Contributor

Summary

Allow healthy runtime heartbeats to recover the TUI status feed after a lease timeout or an explicit unavailable event. Recovery clears the unavailable banner, renews the lease, and allows subsequent runtime events to update the UI.

Technical details

Recovery preserves cleared lifecycle state and progress tombstones instead of resurrecting pre-gap observations. A transcript notice explains that runtime status has resumed but earlier agent, child, compaction, and storage state remains unknown; consecutive healthy heartbeats do not repeat it.

@kit-code-agent kit-code-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heartbeat recovery clears the unavailable indicator without restoring the session association required to accept subsequent lifecycle and progress events.

Comment thread src/tui/app.rs
Comment on lines +1945 to +1946
self.progress_unavailable = false;
self.progress_last_frame = Some(Instant::now());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Restore session routing before declaring runtime recovery

In an initialized TUI session, a lease timeout or unavailable event calls invalidate_runtime_status(), which clears runtime_session_id while leaving session_id set. This heartbeat branch clears the unavailable flag but does not restore that association, so the guard at line 1967 continues rejecting every subsequent agent, child, compaction, and progress event. Both ACP implementations emit SessionStarted when attaching a session, not on healthy heartbeats or subsequent prompts. Consequently, after a transient gap the UI says runtime status resumed, but fresh observations remain invisible for the existing session; only storage updates bypass the session guard.

Restore or re-establish the runtime session association as part of recovery while preserving cross-session filtering, cleared lifecycle state, and progress tombstones. Add a recovery regression that calls start_session, receives the matching SessionStarted, loses transport, and then receives a healthy heartbeat followed by fresh lifecycle/progress events. The current recovery fixtures leave session_id unset, bypassing the guard that prevents production recovery.

For agents:
Validate the following issue, address if needed:

<comment>Healthy heartbeat recovery leaves runtime_session_id cleared, so initialized sessions continue rejecting fresh lifecycle and progress events despite reporting recovery.</comment>
<file_context>
diff --git a/src/tui/app.rs b/src/tui/app.rs
--- a/src/tui/app.rs
+++ b/src/tui/app.rs
@@ -1935,19 +1935,26 @@ impl App {
         // Check expiry before any frame can refresh the lease or revive a
         // lifecycle map. Loss applies to all runtime events, not only progress.
         self.progress_activity();
+        if let RuntimeEvent::RunletTransport { available } = event {
+            if available {
+                if self.progress_unavailable {
+                    // A heartbeat restores transport, not the observations lost
+                    // during the gap. Keep cleared state and progress tombstones.
+                    self.note("Runtime status resumed; earlier agent, child, compaction and storage state remains unknown");
+                }
+                self.progress_unavailable = false;
+                self.progress_last_frame = Some(Instant::now());
+            } else {
+                self.disable_runtime();
+            }
+            return;
+        }
         if self.runtime_unavailable() {
             return;
         }
         let parent = event.parent_call().map(str::to_string);
         let owner_id = match event {
-            RuntimeEvent::RunletTransport { available } => {
-                if available {
-                    self.progress_activity();
-                } else {
-                    self.disable_runtime();
-                }
-                return;
-            }
+            RuntimeEvent::RunletTransport { .. } => unreachable!(),
             RuntimeEvent::StorageStatus { pending, exhausted } => {
                 self.storage_pending = pending;
                 self.storage_exhausted = exhausted;
</file_context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant