Skip to content

fix: reseed emptied workspaces after agent inventory settles - #18886

Closed
nwparker wants to merge 3 commits into
mainfrom
nwparker/fix-empty-workspace-after-inventory
Closed

nwparker wants to merge 3 commits into
mainfrom
nwparker/fix-empty-workspace-after-inventory

Conversation

@nwparker

@nwparker nwparker commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​26 $\color{#cf222e}{\Huge{\mathbf{−}}}$​2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​24
Prod 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​37 $\color{#cf222e}{\Huge{\mathbf{−}}}$​25 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​12

ELI5

Orca lets you close the last terminal in a workspace, and it remembers you did that on purpose so it doesn't keep popping a new one back at you. But when you later click that workspace open again, you're asking for it back — so you should get a fresh terminal.

That only worked some of the time. When Orca first had to check in the background whether any agent sessions were still running there, the check came back "nothing running" and then Orca forgot the last step: actually creating the terminal. You clicked the workspace and got a blank screen with nothing to type into, and had to add a terminal by hand.

The background check now finishes the job the same way the normal path already did, so opening an emptied workspace gives you a usable terminal either way.

Opening a workspace after its last terminal is retired can leave a blank workspace. The asynchronous agent-inventory check correctly reports no sessions, but its continuation omits reseedEmptiedWorkspace, so the closed-tab tombstone prevents the fallback terminal from being created.

Pass the same reseeding option used by normal explicit activation after the inventory gate reports empty. Move the existing folder seeding helper into the terminal-seeding module to keep the activation module within its line limit; folder behavior is unchanged. Correct the worker-retirement E2E setup expectation to done, matching its explicit done event and the durable recovery contract.

Validation: the new asynchronous-inventory regression fails before the fix; 27 focused unit tests pass afterward, including folder and remote activation cases. Targeted lint and pnpm tc:web pass. The existing worker-retirement E2E reproduced the blank workspace before the fix. A separate e2e build now passes both retirement modes; two further runs of both modes plus the whole-tab close/restart scenario passed (8 rendered cases total).

Application behavior change: left open for user review; do not merge automatically.

Reliability: agent-session.provider-ownership covers the invariant that explicit activation may seed an emptied workspace only after session inventory resolves empty; existing sessions still retain ownership. The new unit oracle controls the asynchronous inventory and asserts terminal creation, while the rendered oracle closes/retires terminals and reopens the workspace. No new polling, provider scans, timers, or subprocesses were added. macOS local/daemon behavior is covered by the eight rendered cases; folder and remote activation branches have focused unit coverage. Rendered Linux, Windows, SSH, WSL, and mobile/relay validation remain gaps. Playwright traces and the asynchronous inventory regression provide failure diagnostics.

Tradeoffs

These are the genuine user-facing costs of making the gated path re-seed:

  • Reopening a deliberately-emptied workspace now always costs a shell. The seeded tab carries pendingActivationSpawn, so a PTY is spawned. On an SSH or relay host that is a remote shell process on the server, not just a local one. In the gated path you previously got nothing and paid nothing.
  • A workspace can no longer be kept empty as a "bookmark." Users who close the last terminal and reopen the workspace only to read a diff or browse files get a terminal back on every open, unless the entry point opts out via providesInitialSurface.
  • Narrow ordering race on slow inventory. The continuation only re-checks that the workspace is still active — not which view is showing. If the inventory RPC is slow and you switch to a non-terminal surface inside the same workspace during that window, the seeded terminal can still appear and take tab focus. Bounded, not eliminated: seeding re-reads renderableTabCount and bails if any surface appeared meanwhile, and bails entirely when the execution host owns terminal creation (hostAuthority === 'live'), which covers the live-remote case.
  • Slightly larger unhandled-rejection surface. gateWorktreeAgentActivation(...).then(...) still has no .catch() (pre-existing on both call sites), and the continuation now does more work — tab creation plus store writes — inside it. A throw there becomes an unhandled rejection instead of a no-op.
  • Regression risk is concentrated in one flag. The behavior difference is entirely reseedEmptiedWorkspace: true on one call. The blast radius is every non-providesInitialSurface activation that hits the async agent-inventory gate, which is the common sidebar/palette open path — high traffic, so a mistake here would be widely felt even though the change is one line.

Not affected: no wire/RPC/IPC payload, persisted schema, or platform-specific code changes; folder-workspace behavior is unchanged (that path already passed the flag).

Visual Proof

N/A — no rendered UI surface changes. The fix restores an existing code path
(creating the workspace's fallback terminal); it introduces no new component,
layout, styling, or copy. Local Electron launches are paused for this task per
AGENTS.md, so rendered validation is deferred to CI, where the existing
completed-worker-retirement-resume e2e spec covers the reopen-after-retirement
flow that originally exposed the blank workspace.

Note: why the e2e expectation moves to done

tests/e2e/completed-worker-retirement-resume.spec.ts drives setAgentStatus
twice on the same pane key — working, then done — and asserted the recovery
record stayed working. That was correct until #16308 ("stop ghost resume tabs
after finished turns", c60d2ba8958) added existing.state === next.state to
recoveryRecordMatches
(src/renderer/src/store/slices/agent-status-recovery-equivalence.ts), so the
done update now correctly replaces the working checkpoint.

That same commit already flipped this expectation in the sibling unit test
tests/e2e/completed-worker-retirement-resume.unit.test.ts, but missed the
Playwright spec's copy — this spec only runs when it changes, so the stale
expectation went unnoticed. Recording done is the desired behavior: it is what
marks passive completed hibernation evidence and suppresses auto-resume, which is
exactly what this spec's later assertions (resumeClaimCount: 0, no --resume)
require. Nothing on this branch touches the agent-status-* slices.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 96427cf1-2fd9-40dc-ad2e-d3d4bd38aef6

📥 Commits

Reviewing files that changed from the base of the PR and between e97d3c5 and 6ef94b0.

📒 Files selected for processing (1)
  • src/renderer/src/lib/worktree-initial-terminal-seeding.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/lib/worktree-initial-terminal-seeding.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The change adds ensureFolderWorkspaceInitialTerminal and delegates folder workspace terminal creation to shared seeding logic. Worktree activation enables reseeding when the agent activation gate confirms an emptied workspace. Renderer tests verify terminal reseeding and caller wiring. An end-to-end test updates the expected completed worker recovery state from working to done.

Merge Risk: 🟡 Moderate · up to 6ef94

Workspace recovery can restore a terminal after a newer activation intentionally chose a non-terminal surface, producing an unexpected workspace layout. This ordering issue should be resolved before merge.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed and covers the problem, implementation, tradeoffs, visual proof, testing, platform gaps, and review guidance. However, it omits the required Linked Issue section and issue … Add a Linked Issue section with a valid issue reference, complete the Agent skill upstream boundary checkbox, and include or explicitly address the required checklist items. Confirm the AI Disclosure and Review sections according to reposit…
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: reseeding emptied workspaces after asynchronous agent inventory completes.
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.
Full details: Description check

Explanation

The description is detailed and covers the problem, implementation, tradeoffs, visual proof, testing, platform gaps, and review guidance. However, it omits the required Linked Issue section and issue reference, and it does not include the required template checklist sections or Agent skill upstream boundary checkbox.

Resolution

Add a Linked Issue section with a valid issue reference, complete the Agent skill upstream boundary checkbox, and include or explicitly address the required checklist items. Confirm the AI Disclosure and Review sections according to repository policy.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch nwparker/fix-empty-workspace-after-inventory

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.

@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

🧹 Nitpick comments (1)
src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts (1)

45-47: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert that the test exercises the asynchronous path.

The final tab count alone does not prove that the gate returned empty or that reseeding occurred in the continuation. Assert the pre-gate tab count and the gate outcome.

Proposed test strengthening
     activateAndRevealWorktree(worktree.id, { notifyHostRuntime: false })
-    await waitForWorktreeAgentActivationGateForTests(worktree.id)
+    expect(useAppStore.getState().tabsByWorktree[worktree.id]).toHaveLength(0)
+    const outcome = await waitForWorktreeAgentActivationGateForTests(worktree.id)
+    expect(outcome).toBe('empty')

     expect(useAppStore.getState().tabsByWorktree[worktree.id]).toHaveLength(1)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: da25d922-28d3-498a-a98c-1fcc76416577

📥 Commits

Reviewing files that changed from the base of the PR and between 1924c8f and ab92bbc.

📒 Files selected for processing (4)
  • src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts
  • src/renderer/src/lib/worktree-activation.ts
  • src/renderer/src/lib/worktree-initial-terminal-seeding.ts
  • tests/e2e/completed-worker-retirement-resume.spec.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.

undefined,
undefined,
undefined,
{ reseedEmptiedWorkspace: true }

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Revalidate the activation intent before reseeding.

This delayed callback uses the earlier activation's options. If a second activation sets providesInitialSurface: true before the gate resolves, activeWorktreeId remains the same and this call still creates a terminal. Track the latest activation generation, or revalidate the current activation intent before reseeding.

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

✅ No new issues found.

Reviewed changes

  • Async-gate reseed — activateAndRevealWorktree's outcome === 'empty' continuation now passes { reseedEmptiedWorkspace: true } to ensureWorktreeHasInitialTerminal, matching the explicit-activation path (worktree-activation.ts:281) and the folder path, so a worktree whose last terminal was closed re-seeds a fallback terminal once the async agent inventory confirms it is empty.
  • Folder helper relocation — ensureFolderWorkspaceInitialTerminal moves verbatim from worktree-activation.ts into worktree-initial-terminal-seeding.ts to keep the activation module under its line limit; the FolderWorkspace/folderWorkspaceKey imports move with it, and both call sites in activateAndRevealFolderWorkspace are unchanged in behavior.
  • Regression test — a new focused test drives the async inventory gate against an emptied worktree with a closed-last-terminal tombstone and asserts a re-seed. I traced the without-fix path: the tombstone is honored (shouldHonourClosedTerminalTombstone), so no terminal is created and the assertion fails — the test is real coverage, not theatre.
  • E2E expectation correction — the worker-retirement spec now expects state: 'done', matching the explicit done event its setup writes last, which is what the persisted recovery record holds.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

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

✅ No new issues found.

Reviewed changes

  • Seam census updated — added worktree-initial-terminal-seeding.ts to the SEAM_FILES census in worktree-activation-surface-caller-wiring.test.ts, so the ratchet that requires every providesInitialSurface mention under src/ to be listed stays green now that ensureFolderWorkspaceInitialTerminal lives in that module. Placement in SEAM_FILES (not SURFACE_PROVIDING_CALLERS) is correct: the file declares and forwards the seam via reseedEmptiedWorkspace: providesInitialSurface !== true rather than opting out of tombstone re-seeding. Verified against the source tree that every production mention is now accounted for.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

…ding

No behavior change; the moved helper returned a local it never used.
@nwparker

nwparker commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

#18886 — yes, close it

Unambiguously redundant. Main already has reseedGatedEmptyWorkspace (commit 06a607a, #16904) wired into both gate continuations, and the e2e hunk verbatim. My PR fixes a bug that's already fixed.

I'd salvage one thing: my new test drives the real runWorktreeAgentActivationGate with a stubbed window.api, whereas main's equivalent mocks the gate — strictly better coverage of the same behavior. Land that as a small test-only PR, or just close #18886 outright if you'd rather not spend another review cycle. Either is defensible; the fix itself is dead either way.

Worth naming the process failure: I reviewed #18886 in depth and recommended keeping it, without ever checking its base against current main. It was 227 commits stale, and the fix had landed underneath me. My earlier "keep it, this is a real inconsistency" was confidently wrong for that reason. The same staleness check would have saved most of the work on it — and it's the same class of gap as the missing post-merge gate on main: nothing in the loop compares a branch against reality until CI complains.

@nwparker nwparker closed this Sep 7, 2026
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