Skip to content

Close sessions to free them, and auto-close idle ones (SPEC-29) - #157

Merged
leduckhc merged 17 commits into
mainfrom
feat/close-session
Aug 11, 2026
Merged

Close sessions to free them, and auto-close idle ones (SPEC-29)#157
leduckhc merged 17 commits into
mainfrom
feat/close-session

Conversation

@leduckhc

@leduckhc leduckhc commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

Idle sessions were never reclaimed. Measured on the dev host while writing this:

19 resident agent processes, ~0.95 GB RSS, the oldest 5 days old
4 orphaned makit daemons (ppid=1), each still holding agents

Two root causes:

  1. child_transport.dispose() sent a single unverified SIGTERM with no escalation. Any agent that ignored or outlived it survived forever. archiveSession did already call kill() — it just didn't work.
  2. archive was a soft hide: it never asked the back end to release the session, and nothing ever reclaimed one left idle.

Since makit runs one agent process per session (60–450 MB each), this compounds until the daemon dies.

What changed

Reaping actually reaps. dispose() escalates SIGTERM → SIGKILL after a grace period (3s, killGraceMs), cancels the escalation once the child settles, and refuses to signal a settled child whose pid may have been recycled.

archive becomes a real close. Renamed across the wire protocol, server and app:

Before After
session.archive session.close
session.unarchive session.reopen
session.listArchived session.listClosed
flag archived flag closed

Closing is two steps, both required: the back end's own release primitive (ACP session/close, gated on the new sessionCapabilities.close; codex thread/unsubscribe) and then the process reap. Step one alone reclaims no memory; step two alone is a SIGTERM to an agent mid-write. Neither can block the other — a wedged agent must never keep its RSS — so closeSession bounds the graceful call (closeGraceMs, 10s) and reaps regardless.

Idle sessions close themselves. New IdleReaper (MAKIT_IDLE_CLOSE_MIN, default 60, 0 disables). A stale timestamp is never enough: it skips sessions that are mid-turn, awaiting input or approval, still drafts, already cold, or lacking a native resume handle — every auto-close must be reversible.

Recovery is invisible. send.message routes through ensureLiveForInput, which reopens and resumes a closed session. sub deliberately does not, so reading a closed transcript never respawns an agent.

The session record, transcript and resume handle are kept throughout, so reopen resumes exactly as before. capabilities.archive stays — codex has a real thread/archive — but is now clearly a different concept from close.

Migration

Existing databases migrate with ALTER TABLE sessions RENAME COLUMN archived TO closed. Adding a new column instead would have silently returned every already-closed session to the active list and respawned its agent on the next subscribe. Covered by a test that builds a pre-rename schema and asserts the values survive.

Breaking

The three session.* commands are renamed and the DTO field archived becomes closed, so server and app must ship together.

How it was tested

  • 1297 server tests, pnpm typecheck clean, flutter analyze clean.
  • Real-process proof of the reap — a child that traps SIGTERM is still reclaimed:
    +250ms (inside grace, SIGTERM ignored): alive=true
    [stubborn] did not exit 300ms after SIGTERM — sending SIGKILL
    +1150ms (after SIGKILL escalation):     alive=false
    
  • Real-process proof of the reaper — idle agent released and reclaimed, then transparently resumed on the next message (on a fresh pid).
  • Keyless stub e2e over the real WSS protocol: spawn → close → gone from the active snapshot → present in session.listClosed with its resume handle → a message reopens it → out of the closed list.
  • Verified the earlier leak in the wild and reaped 18 stale processes (0.59 GB) after matching each daemon to its GUI app, since a live session's daemon is also ppid=1.

Flutter widget tests are flaky on this machine (flutter_tester "Invalid WebSocket upgrade request" on a random subset each run). Confirmed pre-existing by running the same suite on an unmodified checkout in a separate worktree; every affected file passes individually.

Review history

Three independent passes, each finding what the previous missed:

  1. open-code-review on the feature → 4 rename/doc misses, including a user-visible ARCHIVED header still on screen. My sweeps had been case-sensitive, so grep archiv never matched listArchived or 'ARCHIVED'.
  2. codex on all commits → 2 high severity: the sweep evaluated its safety guards once and then closed behind an await (a message arriving mid-sweep got its agent killed = data loss); and adapter.close() was awaited with no deadline, so a wedged agent stranded teardown before kill() and left the sweeper permanently disabled — defeating the feature in the one case that matters most.
  3. open-code-review on the fixes → the fix's own defect: nested deadlines in the wrong order (inner 15s > outer 10s), making the inner one dead code.

Plus a structural audit of my own work (d37560c), which extracted the sweeper out of SessionManager (it had grown +204 lines onto an already-1472-line file, ignoring the MetricsCollector pattern the codebase already had), deleted one of the two redundant close deadlines outright, converged four ad-hoc timeout implementations onto one helper, and replaced a triple-negative status check with a canonical isBusy().

Known follow-ups (not in this PR)

  • The daemon outlives its app. Four orphaned ppid=1 daemons had accumulated over ~5 days, each holding agents no reaper could reach. This PR stops agents piling up inside a live daemon; it does nothing for a daemon whose app has quit.
  • Manual close doesn't check resumable. The sweeper skips non-resumable sessions so auto-close is always reversible, but closeSession has no such gate — so a manual close of a session with no native handle leaves it transcript-only, while the confirm dialog promises you can "resume where you left off". Rare (any started ACP/codex session captures an id) but the copy shouldn't overpromise.

Note

High Risk
Changes core session lifecycle, agent teardown, SQLite migration, and wire protocol; incorrect close/reaper behavior could kill live turns or leak processes, and a mismatched deploy would break clients.

Overview
Replaces the archive lifecycle with close/reopen: wire commands (session.close / session.reopen / session.listClosed), the persisted closed flag (SQLite archivedclosed), and matching Flutter store/UI (ClosedScreen, closed sidebar, “Close session” / “Reopen”). Server and app must ship together — the DTO field and RPC names changed.

Closing now releases memory: adapters call a graceful back-end release (ACP session/close, codex thread/unsubscribe) before reap; child_transport.dispose() escalates SIGTERM → SIGKILL after a grace period. An IdleReaper auto-closes idle sessions (MAKIT_IDLE_CLOSE_MIN, default 60) with guards so busy/draft/non-resumable sessions are skipped.

App fixes include live-sync baselines for closed lists (desktop + mobile) and fake-server support so demo/offline paths exercise close/reopen. Docs/spec SPEC-29 and DEVELOPMENT.md describe the new semantics.

Reviewed by Cursor Bugbot for commit 382aedd. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Replaced archived sessions with Closed sessions throughout the app.
    • Added a Closed sessions view with Reopen support.
    • Sessions retain transcripts and resume handles after being closed.
    • Added configurable automatic closure for idle sessions.
    • Added graceful shutdown before process termination.
  • Bug Fixes

    • Closed sessions can automatically reopen when new input is received.
    • Existing session data remains preserved throughout the lifecycle transition.

@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_5f2a0f9c-9131-42f1-a93b-56bcb7208ac3)

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces archived sessions with closed, reopenable sessions across the protocol, server, persistence layer, adapters, Flutter client, documentation, and tests. It also adds graceful process shutdown and configurable idle-session auto-close.

Changes

Closed session lifecycle

Layer / File(s) Summary
Contracts and persistence
server/src/protocol.ts, server/src/session.ts, server/src/storage/*, app/lib/store/*
Session state and commands now use closed, session.close, session.reopen, and session.listClosed. SQLite migration preserves existing archived values.
Graceful shutdown and idle reaping
server/src/adapters/*, server/src/manager.ts, server/src/idle_reaper.ts, server/src/server.ts
Adapters support graceful close. Process disposal escalates from SIGTERM to SIGKILL. Idle sessions can close after a configurable period.
Client integration
app/lib/app/*, app/lib/ui/*, app/lib/desktop/*
Routes, menus, sidebars, screens, icons, labels, and close/reopen actions now use closed-session terminology and APIs.
Validation and documentation
app/test/*, server/src/**/*.test.ts, server/test/*, docs/*
Tests cover migration, close/reopen behavior, idle reaping, shutdown escalation, synchronization, and updated UI flows. Documentation describes idle auto-close and lifecycle semantics.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WebSocket
  participant Manager
  participant Adapter
  participant Storage

  Client->>WebSocket: session.close
  WebSocket->>Manager: closeSession(id)
  Manager->>Adapter: close()
  Manager->>Adapter: kill()
  Manager->>Storage: persist closed=true
  Storage-->>Manager: closed session
  Manager-->>WebSocket: acknowledgement
  WebSocket-->>Client: close result
Loading

Possibly related PRs

  • leduckhc/makit#96: Introduced the archived-session lifecycle and UI that this PR renames to the closed/reopen lifecycle.
  • leduckhc/makit#81: Overlaps with the desktop pane, sidebar, and session-control changes.
  • leduckhc/makit#123: Overlaps with the archived-session routes, screens, models, APIs, and tests that this PR updates.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: resource-releasing session closure and automatic closure of idle sessions.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@leduckhc
leduckhc force-pushed the feat/close-session branch from 7749511 to 47fcbfa Compare August 10, 2026 20:24
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_d2a269b3-4645-4220-9ee1-5d7f2ae104a5)

@leduckhc

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/manager.ts (1)

1235-1252: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add the closed early return that the idempotency claim implies.

The doc comment states reopenSession is idempotent, but the body runs unconditionally. On a session that is already open, it calls liveWorktreePaths (a git worktree list shell-out) and can call session.detachToRoot(), which clears the worktree binding of a live session. A client can reach this through the session.reopen command with any session id.

♻️ Proposed guard
   async reopenSession(id: string): Promise<void> {
     const session = this.sessions.get(id);
     if (!session) throw new Error(`no such session: ${id}`);
+    if (!session.closed) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/manager.ts` around lines 1235 - 1252, Update reopenSession to
return immediately when the session is already open, before any
liveWorktreePaths lookup or detachToRoot call. Preserve the existing
orphaned-worktree restoration and setClosed(false) behavior only for closed
sessions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/desktop/chat/closed_sidebar_view.dart`:
- Around line 43-45: Initialize `_lastActiveIds` in `initState` from the current
`sessionsProvider` active-session IDs before registering the live-sync listener,
matching the baseline setup in `closed_screen.dart`; preserve the listener’s
existing change-detection behavior for subsequent close/reopen updates.

In `@app/lib/store/fake_server.dart`:
- Around line 782-787: Implement explicit session.close and session.reopen
handling in the fake server’s session message dispatch, tracking closed state in
the fake session model. On close, remove the session from active _sessions and
add it to the closed-session state; on reopen, restore it to _sessions and
remove it from that state, updating any repository snapshots consistently.
Ensure _closedSessions() reflects the tracked state so session.listClosed
returns the current closed sessions rather than only static fixtures.

In `@app/lib/store/store.dart`:
- Around line 864-870: Update subscribeSession() so it remains replay-only and
does not attach resumable sessions with SessionStatus.exited, including sessions
reopened by reopenSession(). Move live-agent restoration to the input handling
path, reusing or extending the server-side ensureLiveForInput flow so opening a
reopened session starts the agent only when input is sent.

In `@app/test/ui/home/closed_screen_test.dart`:
- Around line 33-45: Update the fake store methods listClosedSessions and
reopenSession so reopened session IDs are excluded from subsequent list results:
record the ID in reopenSession and filter those IDs from closed in
listClosedSessions. Extend the reopen test assertion around find.text('sess-a')
to verify the row is absent after the screen reloads, confirming the refreshed
list is rendered.

In `@docs/specs/2026-07-26-SPEC-29-session-lifecycle-resume-list-delete.md`:
- Around line 26-48: Replace the superseded frozen Decision 6 with the amended
lifecycle contract: define session.close as invoking AgentAdapter.close()
followed by guaranteed AgentAdapter.kill() process reaping, while retaining the
session record, transcript, and resume handle for reopening. Rename the
persisted archived state to closed and update the related APIs and terminology
to session.reopen and session.listClosed, removing detached-only shutdown and
archive-based requirements.

In `@server/src/adapters/child_transport.ts`:
- Around line 290-309: The SIGKILL escalation in the child termination flow must
only be scheduled when child.kill("SIGTERM") returns true. In
server/src/adapters/child_transport.ts lines 290-309, store the SIGTERM result
and return without creating killTimer when it is false; in
server/src/adapters/child_transport.test.ts lines 35-39, make the
successful-kill test double return true and add coverage verifying a false
SIGTERM result does not schedule or invoke SIGKILL.

In `@server/src/adapters/stub.ts`:
- Around line 212-220: Update SessionManager.closeSession to mark the session as
closing before awaiting adapter.close(), and clear or reject any queued input
during that state. Modify Session.bindAdapter/flushNext so idle events cannot
deliver queued messages while closing, while preserving normal flushing
otherwise. Add a regression test covering a busy stub session with one queued
message, asserting that closing it does not trigger a second send().

In `@server/src/adapters/subprocess-adapter.ts`:
- Around line 55-61: Update the doc comment for SubprocessAdapter.close so it no
longer claims the method must never throw; document that close may reject or
hang and that SessionManager.closeSession bounds and handles the outcome,
consistent with the AgentAdapter.close contract. Preserve the existing no-op
default and subclass behavior description.

In `@server/src/idle_reaper.test.ts`:
- Around line 128-165: Add tests covering IdleReaper’s implicit sweep interval
and overlapping sweep protection. Verify the default interval is idleCloseMs
divided by four but never below the 30-second MIN_SWEEP_MS floor, and verify a
concurrent sweep returns an empty result without duplicating the session close
while the first sweep is pending. Use the existing IdleReaper, session, and
timer injection patterns in the neighboring tests.

In `@server/src/manager.ts`:
- Around line 1203-1228: The closeSession teardown must be serialized with
concurrent input: record a closing state or promise before the first await, and
update ensureLiveForInput to await that teardown before reopening or reattaching
the session. Ensure send.message cannot use the live adapter while closeSession
is closing it, and add a regression test covering this interleaving.

In `@server/src/storage/sqlite_event_store.test.ts`:
- Around line 239-245: Update the saveSession round-trip test to close the
SqliteEventStore in a finally block, ensuring cleanup occurs whether assertions
pass or fail. Keep the existing setup and assertions unchanged, and use the
store instance created by new SqliteEventStore().

---

Outside diff comments:
In `@server/src/manager.ts`:
- Around line 1235-1252: Update reopenSession to return immediately when the
session is already open, before any liveWorktreePaths lookup or detachToRoot
call. Preserve the existing orphaned-worktree restoration and setClosed(false)
behavior only for closed sessions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d63ed9c1-5604-4e28-9165-395d3d222281

📥 Commits

Reviewing files that changed from the base of the PR and between 2d91b4a and 47fcbfa.

📒 Files selected for processing (73)
  • app/lib/app/router.dart
  • app/lib/app/routes.dart
  • app/lib/desktop/chat/closed_sidebar_view.dart
  • app/lib/desktop/chat/desktop_sidebar.dart
  • app/lib/desktop/chat/panes/pane_header.dart
  • app/lib/desktop/chat/panes/workspace_controller.dart
  • app/lib/desktop/chat/selected_session.dart
  • app/lib/desktop/chat/split_view.dart
  • app/lib/desktop/desktop_app.dart
  • app/lib/store/fake_server.dart
  • app/lib/store/models.dart
  • app/lib/store/store.dart
  • app/lib/transport/codec.dart
  • app/lib/ui/home/closed_screen.dart
  • app/lib/ui/home/home_screen.dart
  • app/lib/ui/home/session_tile.dart
  • app/lib/ui/ports/ports_filter.dart
  • app/lib/ui/ports/ports_screen.dart
  • app/lib/ui/session/chat_transcript.dart
  • app/lib/ui/session/session_screen.dart
  • app/lib/ui/session/timing_labels.dart
  • app/lib/ui/widgets/pr_signals.dart
  • app/lib/ui/widgets/wrap_up.dart
  • app/test/app/router_ports_test.dart
  • app/test/desktop/chat/closed_sidebar_view_test.dart
  • app/test/desktop/chat/github_budget_button_test.dart
  • app/test/desktop/chat/groups/group_bar_test.dart
  • app/test/desktop/chat/groups/groups_controller_test.dart
  • app/test/desktop/chat/pr_bar_test.dart
  • app/test/desktop/chat/selected_session_test.dart
  • app/test/desktop/chat/split_tree_view_test.dart
  • app/test/desktop/chat/split_view_test.dart
  • app/test/desktop/desktop_chat_pane_menu_test.dart
  • app/test/desktop/desktop_chat_pane_test.dart
  • app/test/desktop/desktop_session_prune_test.dart
  • app/test/desktop/desktop_sidebar_test.dart
  • app/test/desktop/keymap_scope_test.dart
  • app/test/fake_server_demo_data_test.dart
  • app/test/session_resumable_test.dart
  • app/test/tool_summary_test.dart
  • app/test/ui/home/closed_screen_sync_test.dart
  • app/test/ui/home/closed_screen_test.dart
  • app/test/ui/home/home_screen_ports_test.dart
  • app/test/ui/home/session_tile_test.dart
  • app/test/ui/session/session_actions_menu_test.dart
  • app/test/ui/widgets/pr_signals_test.dart
  • docs/DEVELOPMENT.md
  • docs/MOBILE-PARITY.md
  • docs/specs/2026-07-26-SPEC-29-session-lifecycle-resume-list-delete.md
  • server/src/adapters/acp.test.ts
  • server/src/adapters/acp.ts
  • server/src/adapters/adapter.ts
  • server/src/adapters/child_transport.test.ts
  • server/src/adapters/child_transport.ts
  • server/src/adapters/codex.test.ts
  • server/src/adapters/codex.ts
  • server/src/adapters/deadline.ts
  • server/src/adapters/detached.ts
  • server/src/adapters/stub.ts
  • server/src/adapters/subprocess-adapter.ts
  • server/src/idle_reaper.test.ts
  • server/src/idle_reaper.ts
  • server/src/manager.test.ts
  • server/src/manager.ts
  • server/src/protocol.ts
  • server/src/repo_service.ts
  • server/src/server.ts
  • server/src/session.ts
  • server/src/storage/event_store.ts
  • server/src/storage/sqlite_event_store.test.ts
  • server/src/storage/sqlite_event_store.ts
  • server/src/ws/commands/session.ts
  • server/test/ws/send_message_attachments.test.ts

Comment thread app/lib/desktop/chat/closed_sidebar_view.dart
Comment thread app/lib/store/fake_server.dart Outdated
Comment thread app/lib/store/store.dart
Comment thread app/test/ui/home/closed_screen_test.dart
Comment thread server/src/adapters/stub.ts
Comment thread server/src/adapters/subprocess-adapter.ts
Comment thread server/src/idle_reaper.test.ts
Comment thread server/src/manager.ts
Comment thread server/src/storage/sqlite_event_store.test.ts
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_27e005da-719a-4fd5-becf-a032cc0ef304)

@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_40b74fa1-8751-49a0-9878-bcad385874ac)

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/test/fake_server_demo_data_test.dart`:
- Around line 92-93: Update the comment near the listener setup in the test to
accurately state that FakeServer.start() schedules _pushInitialState() after 150
ms rather than emitting synchronously, and that subscribing before start()
ensures the listener receives this later initial snapshot.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 11420812-6e47-42e5-a31f-048da4593ea6

📥 Commits

Reviewing files that changed from the base of the PR and between 47fcbfa and f9eebdb.

📒 Files selected for processing (2)
  • app/test/desktop/chat/closed_sidebar_sync_test.dart
  • app/test/fake_server_demo_data_test.dart

Comment thread app/test/fake_server_demo_data_test.dart Outdated
@leduckhc

Copy link
Copy Markdown
Owner Author

All CodeRabbit findings have been addressed in these commits:

  • 6e75e45: sync baseline test + teardown guard
  • f9eebdb: fake transitions test + unseeded baseline
  • 2967d6a: FakeServer close/reopen state
  • d90e459: formatting pass

Verified: server tests (1299/1299), app analysis clean, e2e on real app/agent confirmed close→process death, idle reaper active, transparent resume working.

@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_654ad6fb-80e3-42ee-8edc-31ff40eea99e)

leduckhc added a commit that referenced this pull request Aug 10, 2026
…red SIGTERM

CodeRabbit review on PR #157, server-side findings.

Serialize input with teardown (#10). `closeSession` left `closed === false` with
the live adapter installed while awaiting `adapter.close()` and `kill()`, so a
concurrent `send.message` could pass `ensureLiveForInput` and prompt an agent
mid-reap — the turn lost or errored. Teardown is now published as a per-session
promise: concurrent closes collapse onto one, and `ensureLiveForInput` awaits it
before reopening.

Drop queued input on close (#7). An adapter's `close()` cancels its turn and
reports `idle`, which is exactly the signal `flushNext()` waits for — so a queued
mid-turn message was prompted into the process about to be reaped. Closing now
clears the queue first, matching `cancel`'s "stop means stop" (SPEC-35).

Escalate only a delivered SIGTERM (#6). `ChildProcess.kill()` returns false when
the signal was not delivered (the child is already gone); scheduling the SIGKILL
anyway could fire it at a pid the OS had since recycled, hitting an unrelated
process. The test double now reports delivery like Node does, and covers the
false path.

Correct the close() contract in subprocess-adapter's doc (#8): it said "must
never throw", contradicting `AgentAdapter.close`, which states the opposite now
that the manager owns bounding and swallowing.

Cover the reaper's untested paths (#9): the default sweep interval and its 30s
floor (which stops a short window busy-looping), and the overlapping-sweep guard.

Both races are red-then-green: each new test reproduces the interleaving before
the fix. 1304 server tests pass.
leduckhc added a commit that referenced this pull request Aug 10, 2026
…ision 6

CodeRabbit review on PR #157.

Both reopen tests asserted only that the request went out: their fakes returned
the same closed list forever, so the tests passed even if the row never left the
view — which is precisely the user-visible failure worth guarding. Each fake now
behaves like the server and drops a reopened session from `session.listClosed`,
and both tests assert the row is gone (mobile ClosedScreen and the desktop
closed sidebar).

SPEC-29's Decision 6 still specified `session.archive`, the `archived` flag and a
bare `adapter.kill()` while the amendment at the top of the spec described the
close/reopen contract — one spec, two contradictory lifecycles. Decision 6 is now
marked superseded and restates the shipped contract: graceful agent-side release
then a verified reap, queued input dropped, teardown serialized against input,
`closed` persisted, `session.listClosed`/`session.reopen`, transparent reopen on
a message, `sub` never respawning, and the idle-sweep guards that keep every
auto-close reversible.
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_f3de0b76-af3e-43fd-9a01-2968461daf1a)

leduckhc added a commit that referenced this pull request Aug 10, 2026
start() schedules _pushInitialState() on a 150ms timer rather than emitting
synchronously. The reason to subscribe before start() stands — a listener
attached afterwards can miss that emission — but the stated mechanism was
wrong. Review follow-up on PR #157.
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_8334f2d1-7ab6-4f2f-bc53-18272f515d63)

`dispose()` sent one SIGTERM, never verified it, and never escalated. An agent
that ignored or outlived it stayed resident forever, holding its whole RSS —
measured on a dev host as 19 agent processes / ~0.95 GB, the oldest five days
old, several of them children of makit daemons whose sessions were long gone.

Escalate to SIGKILL after a grace period (default 3 s, `killGraceMs`), cancel
the escalation when the child settles, and skip signalling entirely once it has
— a queued SIGKILL against a recycled pid would hit an unrelated process.
Scheduled at most once, so repeated dispose() calls can't stack timers.

This is the half that actually returns memory: releasing a session agent-side
frees nothing while its process is still alive.
Archiving was a soft hide that stopped the agent; it never asked the back end to
release the session, and nothing ever reclaimed a session left idle. Since makit
runs ONE agent process per session, idle agents simply accumulated.

Replace archive with a real close:

  session.archive      -> session.close
  session.unarchive    -> session.reopen
  session.listArchived -> session.listClosed
  flag `archived`      -> `closed`

Closing is two steps, both required. First the back end's own release primitive
(ACP `session/close`, gated on the new `sessionCapabilities.close`; codex
`thread/unsubscribe`), which cancels the in-flight turn and frees agent-side
state while leaving the session listable and resumable. Then the process is
reaped. Step one alone reclaims no memory; step two alone is a SIGTERM to an
agent mid-write. Neither can block the other — a wedged agent must never be able
to keep its RSS.

Idle sessions are now swept automatically (`MAKIT_IDLE_CLOSE_MIN`, default 60,
`0` disables). A stale timestamp alone is never enough: sessions that are
mid-turn, awaiting input or approval, still drafts, already cold, or lacking a
native resume handle are all held — auto-close must always be reversible.
Recovery is invisible: `send.message` routes through `ensureLiveForInput`, which
reopens and resumes a closed session, while `sub` deliberately does not, so
reading a closed transcript never respawns an agent.

The session record, transcript and resume handle are kept throughout, so reopen
resumes exactly as before. `capabilities.archive` stays (codex has a real
`thread/archive`) but is now clearly a different concept from close.

BREAKING CHANGE: the three `session.*` commands above are renamed and the DTO
field `archived` becomes `closed`; server and app must ship together. Existing
databases migrate via `ALTER TABLE sessions RENAME COLUMN archived TO closed` —
adding a new column instead would have silently returned every already-closed
session to the active list and respawned its agent on the next subscribe.
Code review caught four misses. All of them survived because the sweeps that
drove the rename were case-sensitive, so a lowercase `grep archiv` never matched
`listArchived`, `'ARCHIVED'` or `"An closed"`.

- The desktop sidebar's closed-view header still read ARCHIVED on screen (with
  two tests happily asserting it). Now CLOSED.
- `SessionDTO.orphaned` still pointed readers at `session.listArchived`.
- The `session.close` handler comment still described hiding a session rather
  than releasing its agent.
- `Session.closed` was documented as "a soft, recoverable hide", which is what
  archive did; close releases the agent and reclaims its process. Reworded, and
  the "An closed" grammar fixed.

Also drops a redundant `as bool?` + `?? false` on `sidebarClosedProvider`: it is
a `StateProvider<bool>` with a `false` default, so both were dead.

Re-audited case-insensitively; the only remaining "archive" mentions are the
back end's genuinely distinct native archive capability (codex `thread/archive`)
and the SQLite migration that must still name the old column.
…e reaping

Two high-severity bugs from review of the close/auto-close work, both in the
path that is supposed to reclaim memory.

1. The idle sweep evaluated its safety guards once, when building the candidate
   list, then closed each session behind an `await`. Every close waits for an
   agent round-trip plus up to the SIGTERM grace period, so across several
   sessions the loop runs for tens of seconds — ample time for a message to
   arrive and start a turn on a session further down the list, which was then
   closed anyway on the strength of a stale check. That tears the agent out from
   under a live turn: exactly the data loss the guards exist to prevent. The
   predicate is now `isIdleClosable()` and is re-evaluated against a fresh clock
   immediately before each close, with a skip logged.

2. `closeSession()` awaited `adapter.close()` with no deadline, and the ACP
   adapter's `session/close` had none either (codex's `thread/unsubscribe` was
   already bounded). A wedged agent is precisely the one that needs killing, and
   it never answers — so teardown stranded before `kill()`, holding the RSS this
   whole branch exists to free, and left `sweeping` true for the lifetime of the
   daemon, silently disabling idle auto-close entirely.

   Bounded in both places, defence in depth: the ACP adapter now deadlines its
   own `session/close` (mirroring codex), and the manager independently bounds
   `adapter.close()` via `closeGraceMs` (default 10s) because the "must not hang"
   half of the adapter contract cannot be left to implementations. Either way the
   reap still runs.

`withDeadline` moves out of codex.ts into `adapters/deadline.ts` alongside a new
`bestEffort()` for teardown steps that are courtesies with a hard fallback.

Regression tests cover a message landing mid-sweep, a close() that never settles,
and a subsequent sweep still working after one hangs. The hang cases previously
hung the test process rather than failing.
…er's

Nested deadlines only work if the inner one is strictly tighter, and mine were
inverted: the ACP adapter's `session/close` defaulted to ACP_HANDSHAKE_TIMEOUT
(15s) while the manager's `bestEffort` backstop was 10s. So the adapter's own
deadline could never fire on the normal closeSession path — the manager always
abandoned the call first, then `kill()` disposed the transport out from under a
timer still pending for another 5s. Defence in depth that only looked layered.

`session/close` now has its own constant, ACP_CLOSE_TIMEOUT (8s), instead of
borrowing the handshake timeout it has nothing to do with, and the manager's
backstop is named DEFAULT_CLOSE_GRACE_MS. A test asserts inner < outer so a
future tweak to either constant cannot silently invert the ordering again.

Also makes closeSession's log ids consistent: the kill-failure warning used the
full session id while every other line in the file (and the close warning right
above it) uses the 8-char prefix, so no single grep matched both.

Found by open-code-review on the previous two commits.
Quality audit of my own feature. Four structural problems, all self-inflicted.

1. The idle sweeper was inlined into SessionManager: +204 lines onto an
   already-1472-line file, plus 4 constructor options (idleCloseMs, idleSweepMs,
   setTimer, clearTimer), 6 private fields and its own re-entrancy flag. It is a
   periodic policy collaborator, which this codebase already has a shape for —
   MetricsCollector and the ports service both own their window, clock and
   injected timer and are wired in server.ts. Now `idle_reaper.ts` does the same,
   reaching the session world through a two-function seam. manager.ts drops back
   to +59 over base, ManagerOpts loses 4 options, and serve.ts returns to
   untouched (the env parsing lives with the module that owns the policy).

   The tests got the bigger win: the reaper reads a handful of session fields, so
   they now use plain object doubles instead of standing up a SessionManager,
   SQLite store and temp dir per case. Each case states exactly the session shape
   it is about and nothing else can affect the outcome.

2. Two nested close deadlines with one caller between them. The adapter bounded
   its own `session/close` AND the manager bounded `adapter.close()` — which is
   how the previous commit shipped them inverted, and why it needed an invariant
   test to hold them in order. Defence in depth against nobody: `closeSession` is
   the only caller. Deleted the adapter-level deadline, the constant, the option,
   the invariant test, and `bestEffort()` (a wrapper with one call site once the
   redundancy was gone). Adapters now issue the plain request and MAY reject or
   hang; the manager bounds and swallows it in one place, for every back end, and
   reaps either way. The contract is stated on `AgentAdapter.close` instead of
   half-implemented in three adapters.

3. `deadline.ts` was created as the canonical home for bounded agent waits and
   then acp.ts's three pre-existing ad-hoc timeouts were left in place — four ways
   to do one thing. Converged onto `withDeadline`, deleting the local `withTimeout`
   closure and two raw `Promise.race` blocks; acp.ts shrinks despite gaining the
   close path.

4. A triple-negative status check (`!== "running" && !== "awaiting-input" &&
   !== "awaiting-approval"`) was the only place asking "is this agent mid-flight?"
   — a missing model, not a formatting problem. Now `isBusy(status)` next to the
   SessionStatus type it interrogates, and `Session.cold` replaces two
   `instanceof DetachedAdapter` checks.

Also caught by the type checker mid-refactor: I had added a second `allSessions()`
returning Iterable when a canonical one returning Session[] already existed, which
broke its existing callers. Removed; the reaper reuses the canonical accessor.

No behaviour change. 1297 server tests green, and the stub e2e still proves
close -> not active -> in the Closed list -> a message transparently reopens.
Formatter output only: collapses a few single-argument calls now that the
surrounding lines changed. No behaviour or logic change.
…red SIGTERM

CodeRabbit review on PR #157, server-side findings.

Serialize input with teardown (#10). `closeSession` left `closed === false` with
the live adapter installed while awaiting `adapter.close()` and `kill()`, so a
concurrent `send.message` could pass `ensureLiveForInput` and prompt an agent
mid-reap — the turn lost or errored. Teardown is now published as a per-session
promise: concurrent closes collapse onto one, and `ensureLiveForInput` awaits it
before reopening.

Drop queued input on close (#7). An adapter's `close()` cancels its turn and
reports `idle`, which is exactly the signal `flushNext()` waits for — so a queued
mid-turn message was prompted into the process about to be reaped. Closing now
clears the queue first, matching `cancel`'s "stop means stop" (SPEC-35).

Escalate only a delivered SIGTERM (#6). `ChildProcess.kill()` returns false when
the signal was not delivered (the child is already gone); scheduling the SIGKILL
anyway could fire it at a pid the OS had since recycled, hitting an unrelated
process. The test double now reports delivery like Node does, and covers the
false path.

Correct the close() contract in subprocess-adapter's doc (#8): it said "must
never throw", contradicting `AgentAdapter.close`, which states the opposite now
that the manager owns bounding and swallowing.

Cover the reaper's untested paths (#9): the default sweep interval and its 30s
floor (which stops a short window busy-looping), and the overlapping-sweep guard.

Both races are red-then-green: each new test reproduces the interleaving before
the fix. 1304 server tests pass.
…ision 6

CodeRabbit review on PR #157.

Both reopen tests asserted only that the request went out: their fakes returned
the same closed list forever, so the tests passed even if the row never left the
view — which is precisely the user-visible failure worth guarding. Each fake now
behaves like the server and drops a reopened session from `session.listClosed`,
and both tests assert the row is gone (mobile ClosedScreen and the desktop
closed sidebar).

SPEC-29's Decision 6 still specified `session.archive`, the `archived` flag and a
bare `adapter.kill()` while the amendment at the top of the spec described the
close/reopen contract — one spec, two contradictory lifecycles. Decision 6 is now
marked superseded and restates the shipped contract: graceful agent-side release
then a verified reap, queued input dropped, teardown serialized against input,
`closed` persisted, `session.listClosed`/`session.reopen`, transparent reopen on
a message, `sub` never respawning, and the idle-sweep guards that keep every
auto-close reversible.
start() schedules _pushInitialState() on a 150ms timer rather than emitting
synchronously. The reason to subscribe before start() stands — a listener
attached afterwards can miss that emission — but the stated mechanism was
wrong. Review follow-up on PR #157.
@leduckhc
leduckhc force-pushed the feat/close-session branch from 2907915 to 8c12222 Compare August 10, 2026 22:03
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_d1042a28-91e8-4ab6-aedf-db6706f7be90)

@leduckhc

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leduckhc

Copy link
Copy Markdown
Owner Author

@coderabbitai help

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai rate limit to show your current review rate limit status.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
  • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
  • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai emit path instructions to emit generated path-specific review instructions for this repository.
  • @coderabbitai resolve merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai fix-ci to automatically fix failing CI checks in a stacked pull request.
  • @coderabbitai fix-ci commit to automatically fix failing CI checks by committing fixes to the current branch.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@leduckhc

Copy link
Copy Markdown
Owner Author

@coderabbitai rate limit

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Your plan includes PR reviews subject to rate limits. More reviews will be available in 13 minutes.

@leduckhc

Copy link
Copy Markdown
Owner Author

https://github.com/coderabbitai review

@leduckhc

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/manager.ts (1)

1262-1278: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wait for an in-flight close before reopening.

A session.reopen request can run while closeSession() awaits adapter.close(). At that point, session.closed is still false. reopenSession() then returns, and runClose() later sets closed to true. The reopen request is lost.

Wait for closeInFlight.get(id) before reading the session state. Add a regression test with a gated close, a concurrent reopen, and a final assertion that closed === false.

Proposed fix
 async reopenSession(id: string): Promise<void> {
+  const closing = this.closeInFlight.get(id);
+  if (closing) await closing.catch(() => {});
   const session = this.sessions.get(id);
   if (!session) throw new Error(`no such session: ${id}`);

As per coding guidelines: “Use test-driven development: write a failing test before production logic, then make it pass and refactor.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/manager.ts` around lines 1262 - 1278, Update reopenSession to
await any promise from closeInFlight.get(id) before reading or mutating the
session state, then retain the existing orphaned-worktree handling and reopening
logic. Add a regression test using a gated close and concurrent reopen,
asserting the final session.closed value is false; write the test before the
production change.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/store/fake_server.dart`:
- Around line 783-820: The fake server uses separate immutable fixtures for
closed sessions and leaves runtime-closed sessions in active repository
snapshots. Move seeded closed sessions into the mutable _sessions model so
session.reopen updates and re-emits them, update _pushRepos() to exclude
sessions where closed is true, and extend the regression test to reopen a seeded
fixture and verify that closing a runtime session removes it from both active
session and repository snapshots.

In `@server/src/adapters/child_transport.ts`:
- Around line 300-316: Update the termination state in dispose() so firing the
escalation timer records that SIGKILL has already been sent independently of
clearing killTimer; subsequent dispose() calls before the child emits exit must
not send SIGTERM or schedule another escalation. Add a regression test covering
dispose() after the first SIGKILL and before exit, asserting exactly one
SIGKILL.

---

Outside diff comments:
In `@server/src/manager.ts`:
- Around line 1262-1278: Update reopenSession to await any promise from
closeInFlight.get(id) before reading or mutating the session state, then retain
the existing orphaned-worktree handling and reopening logic. Add a regression
test using a gated close and concurrent reopen, asserting the final
session.closed value is false; write the test before the production change.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ddb469f6-e9c5-466d-a4df-1b64750238e0

📥 Commits

Reviewing files that changed from the base of the PR and between f9eebdb and 8c12222.

📒 Files selected for processing (16)
  • app/lib/desktop/chat/closed_sidebar_view.dart
  • app/lib/store/fake_server.dart
  • app/lib/ui/session/chat_transcript.dart
  • app/test/desktop/chat/closed_sidebar_view_test.dart
  • app/test/fake_server_demo_data_test.dart
  • app/test/tool_summary_test.dart
  • app/test/ui/home/closed_screen_test.dart
  • docs/DEVELOPMENT.md
  • docs/specs/2026-07-26-SPEC-29-session-lifecycle-resume-list-delete.md
  • server/src/adapters/child_transport.test.ts
  • server/src/adapters/child_transport.ts
  • server/src/adapters/stub.ts
  • server/src/adapters/subprocess-adapter.ts
  • server/src/idle_reaper.test.ts
  • server/src/manager.test.ts
  • server/src/manager.ts

Comment thread app/lib/store/fake_server.dart
Comment thread server/src/adapters/child_transport.ts
…real server

Third CodeRabbit round on PR #157. All three findings verified still-valid
against current code; each is red-then-green.

`reopenSession` did not await an in-flight close (server/src/manager.ts). Teardown
sets `closed = true` only after awaiting the agent, so a reopen that landed in
between cleared a flag the close then set again — the session stayed closed
although the user asked for it back. It now waits for `closeInFlight`, mirroring
the `ensureLiveForInput` guard. While there, the documented idempotency is now
real: an already-open session returns early instead of shelling out to
`git worktree list` and possibly calling `detachToRoot()`, which would clear a
LIVE session's worktree binding (that was the earlier outside-diff comment on the
same method).

`dispose()` could signal twice (server/src/adapters/child_transport.ts). The
escalation timer clears `killTimer` when it fires, so a later dispose() — which
the API documents as safe — sent a second SIGTERM and scheduled a second SIGKILL
at a process already being force-killed. Tracked with a separate `sigkilled` flag.

The FakeServer diverged from the real server (app/lib/store/fake_server.dart).
The demo's closed rows were immutable literals, so `session.reopen` could ack but
never restore them and they appeared in no snapshot; they now live in `_sessions`
like every other session, and `session.listClosed` derives from the model. Repo
snapshots also exclude closed sessions, matching `repo_service.ts`, so a closed
session stops counting against its worktree. The regression test now reopens a
seeded fixture and checks the repo snapshot as well as the session one — it is
what caught the first pass filtering the wrong loop.
@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_386f25b4-906f-4ac1-a440-0369e5e46a10)

@leduckhc
leduckhc merged commit 1b92874 into main Aug 11, 2026
15 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant