Repository navigation
Hibernate idle background terminal surfaces and cap live surfaces (LRU) #5739
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 1 commit
Commits
Show all changes
64 commits
Select commit
Hold shift + click to select a range
4b2ae3d
Add failing surface hibernation lifecycle policy tests
austinywang 46f2606
Hibernate idle background terminal surfaces behind an LRU cap
austinywang 8838919
Merge remote-tracking branch 'origin/main' into issue-5731-surface-hi…
austinywang e617b49
Document why the agent-panel test helper marks surfaces busy
austinywang db742b8
Track test helper comment in file length budget
austinywang d30745d
Localize surface hibernation schema descriptions
austinywang 9838586
Merge remote-tracking branch 'origin/main' into issue-5731-surface-hi…
austinywang 8083a3b
Track AgentLaunchSanitizer length from main in budget
austinywang 22b11e6
Harden surface hibernation busy gate, settings UI, and idle-tick cost
austinywang 707e695
Persist hibernated scrollback on every autosave; drop input throttle
austinywang 4b0ab2f
Merge remote-tracking branch 'origin/main' into issue-5731-surface-hi…
austinywang 5d40d1e
Persist hibernated working directory in session snapshots
austinywang 874b1c6
True up budget for hibernated-directory snapshot fix
austinywang 80c604e
Protect visible panels, pending command lines, and main's ghostty poi…
austinywang e54ca51
Merge remote-tracking branch 'origin/main' into issue-5731-surface-hi…
austinywang caf6c41
Keep command line pending when text trails the last settling character
austinywang a825af8
Merge remote-tracking branch 'origin/main' into issue-5731-surface-hi…
austinywang f700d46
Clear pending shell input only on observed prompt transitions
austinywang 1f28564
Record input synchronously and restore rendered hibernated panels
austinywang a9167eb
Include shell-integration command state in the hibernation busy gate
austinywang eb462de
Re-read hibernation settings after async work; route mobile input via…
austinywang 68b1b7a
Merge remote-tracking branch 'origin/main' into issue-5731-surface-hi…
austinywang 3e9a576
Convert policy suite to Swift Testing; harden re-enable and cwd staging
austinywang 73f9106
Fix nested shell-state type and make round-trip test hermetic
austinywang 96eab78
Merge remote-tracking branch 'origin/main' into issue-5731-surface-hi…
austinywang 73eec3b
Honor the documented hidden-workspace window on first tail sample
austinywang b5db675
Replay restored scrollback in the fish integration
austinywang 5afca77
Abort hibernation on failed scrollback capture; count prompt transiti…
austinywang 683dc21
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang e1b2c61
Keep batched trailing text pending across the next prompt transition
austinywang bfb83e5
Stage hibernated working directories without touching the disk
austinywang 7c095bb
Quarantine stale surfaces in busy probe; cover paste batches; reapply…
austinywang ead282b
Bound unmounted-idle hibernation batches and stale tail-sampling pres…
austinywang 988908f
Survive one prompt transition per batched command before clearing pen…
austinywang 189d5b0
Restore main-side content reverted by merge mishaps
austinywang c57ce56
True budget entries after restoring main-side content
austinywang f8b094a
Bound global-cap drains, seed pending before evaluation, gate restart…
austinywang 85753cc
Wake hibernated terminals for mobile reads; only text-bearing input a…
austinywang b43f598
Discount prior selections before computing global-cap overflow
austinywang 8299f48
Gate restart on current integration setting; arm pending for text nam…
austinywang 5c1cb8b
Merge remote-tracking branch 'origin/main' into issue-5731-surface-hi…
austinywang fe24526
Key replay capability off the shell name; treat editing controls as n…
austinywang 732b6a4
Write replay file at hibernate time; seed snapshot fallback; cover sp…
austinywang dfc052e
Scope tail-fingerprint sampling to the opt-in agent cap
austinywang dcc6946
Abort hibernation when the replay file cannot be written; clean up ab…
austinywang 7c73096
Purge stale scrollback replay files once per process
austinywang 0f3e304
Scope replay purge per bundle; keep last scrollback for surface-less …
austinywang 05a1ce0
Document the new surface hibernation catalog keys
austinywang 8a0d352
Harden shell-restart gates: non-shell foregrounds and disabled-integr…
austinywang 9646e1f
Fail closed on missing foreground PID and pre-filter process syscalls
austinywang d464421
merge: resolve conflicts with main
austinywang dffa233
Keep the replay-file purge off the calling thread
austinywang ec21726
merge: resolve length-budget conflict with main
austinywang e47ba95
Make pending survivals additive and key replay support off installed …
austinywang 65df0ac
Clear the scrollback fallback when hibernation captures an empty term…
austinywang 722d3a1
Record key input only when it reaches the terminal; reconcile seeded …
austinywang 44ad65b
Own the staged replay file through the whole restore window
austinywang b6523fe
Forward duplicate shell reports to hibernation; repair browser-CLI me…
austinywang b7cab15
Bound per-tick foreground verification to an LRU window
austinywang 668b10d
merge: resolve length-budget conflict with main
austinywang 889a4b9
Cache verified-busy foregrounds and stop arming pending on editing co…
austinywang 21fc212
Requalify seeded shells on bare Enter; keep budget for evictable cand…
austinywang b9acc1a
merge: resolve length-budget conflict with main
austinywang 40c8368
Write replay files off-actor; fix CRLF classification; restore socket…
austinywang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import Foundation | ||
|
|
||
| /// How a live Ghostty runtime surface can be reclaimed by hibernation. | ||
| enum SurfaceHibernationMechanism: Equatable, Sendable { | ||
| /// The panel runs a restorable coding agent. Hibernation terminates the | ||
| /// scoped agent processes and frees the runtime surface; the agent resumes | ||
| /// with its saved session when the panel is visited again. | ||
| case agentResume | ||
| /// The panel is a plain shell whose recreation has no side effects. | ||
| /// Hibernation captures scrollback, frees the runtime surface (which ends | ||
| /// the shell), and starts a fresh shell in the captured working directory | ||
| /// with the scrollback replayed when the panel is visited again. | ||
| case shellRestart | ||
| } | ||
|
|
||
| /// One terminal panel as seen by the surface-lifecycle planner. | ||
| struct SurfaceHibernationPlannerInput: Sendable { | ||
| let key: AgentHibernationPanelKey | ||
| /// nil when the panel must never be hibernated (deferred startup work, | ||
| /// remote PTY attach, tmux-bound, or queued input that suspension would drop). | ||
| let mechanism: SurfaceHibernationMechanism? | ||
| /// Whether the panel currently owns a live Ghostty runtime surface (or, for | ||
| /// agent panels, a live scoped process). Every live input counts toward the | ||
| /// global cap census even when it is exempt from eviction. | ||
| let isLive: Bool | ||
| /// Visible in the selected workspace's rendered layout. | ||
| let isProtected: Bool | ||
| /// The surface reports it is not safely at a shell prompt | ||
| /// (`needsConfirmClose`), e.g. a foreground process is running. | ||
| let isBusy: Bool | ||
| /// Agent lifecycle reported by hooks; `.unknown` for plain shells. | ||
| let lifecycle: AgentHibernationLifecycleState | ||
| let hasUnconfirmedTerminalInput: Bool | ||
| let lastActivityAt: TimeInterval | ||
| /// When the owning workspace last stopped rendering (unmounted), or nil | ||
| /// while it is mounted. | ||
| let workspaceUnmountedAt: TimeInterval? | ||
|
|
||
| init( | ||
| key: AgentHibernationPanelKey, | ||
| mechanism: SurfaceHibernationMechanism?, | ||
| isLive: Bool, | ||
| isProtected: Bool, | ||
| isBusy: Bool = false, | ||
| lifecycle: AgentHibernationLifecycleState = .unknown, | ||
| hasUnconfirmedTerminalInput: Bool = false, | ||
| lastActivityAt: TimeInterval, | ||
| workspaceUnmountedAt: TimeInterval? = nil | ||
| ) { | ||
| self.key = key | ||
| self.mechanism = mechanism | ||
| self.isLive = isLive | ||
| self.isProtected = isProtected | ||
| self.isBusy = isBusy | ||
| self.lifecycle = lifecycle | ||
| self.hasUnconfirmedTerminalInput = hasUnconfirmedTerminalInput | ||
| self.lastActivityAt = lastActivityAt | ||
| self.workspaceUnmountedAt = workspaceUnmountedAt | ||
| } | ||
| } | ||
|
|
||
| /// Pure selection policy for which live terminal surfaces to hibernate. | ||
| /// | ||
| /// Three rules compose, and the result is their union: | ||
| /// - Agent cap: live restorable-agent terminals above | ||
| /// `AgentHibernationSettings.maxLiveTerminals`, oldest first. | ||
| /// - Global LRU cap: live surfaces of any kind above | ||
| /// `SurfaceHibernationSettings.maxLiveSurfaces`, oldest first. The census | ||
| /// counts every live surface — including ones materialized by background | ||
| /// priming or queued socket input — while only eligible ones are evicted. | ||
| /// - Unmounted idle: surfaces whose workspace has been unmounted longer than | ||
| /// `SurfaceHibernationSettings.unmountedIdleSeconds`, regardless of cap | ||
| /// pressure. | ||
| enum SurfaceHibernationPlanner { | ||
| static func selectedPanelKeys( | ||
| inputs: [SurfaceHibernationPlannerInput], | ||
| agentSettings: AgentHibernationSettings.Values, | ||
| surfaceSettings: SurfaceHibernationSettings.Values, | ||
| now: TimeInterval | ||
| ) -> Set<AgentHibernationPanelKey> { | ||
| // This mirrors the shipped AgentHibernationPlanner policy: only | ||
| // restorable-agent terminals are counted or evicted, and only under | ||
| // agent-cap pressure. Plain-shell surfaces are invisible to it, the | ||
| // global cap census does not exist, and workspaces unmounted for long | ||
| // periods keep every runtime surface alive | ||
| // (https://github.com/manaflow-ai/cmux/issues/5731). | ||
| guard agentSettings.enabled else { return [] } | ||
| let liveRestorable = inputs.filter { $0.mechanism == .agentResume && $0.isLive } | ||
| let excess = liveRestorable.count - agentSettings.maxLiveTerminals | ||
| guard excess > 0 else { return [] } | ||
|
|
||
| let eligible = liveRestorable | ||
| .filter { input in | ||
| !input.isProtected && | ||
| input.lifecycle.allowsHibernation && | ||
| !input.hasUnconfirmedTerminalInput && | ||
| now - input.lastActivityAt >= agentSettings.idleSeconds | ||
| } | ||
| .sorted(by: Self.leastRecentlyUsedFirst) | ||
|
|
||
| return Set(eligible.prefix(excess).map(\.key)) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| static func leastRecentlyUsedFirst( | ||
| _ lhs: SurfaceHibernationPlannerInput, | ||
| _ rhs: SurfaceHibernationPlannerInput | ||
| ) -> Bool { | ||
| if lhs.lastActivityAt == rhs.lastActivityAt { | ||
| return lhs.key.panelId.uuidString < rhs.key.panelId.uuidString | ||
| } | ||
| return lhs.lastActivityAt < rhs.lastActivityAt | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.