Skip to content

fix(host-service): make workspace delete reliably tear down terminal sessions#5168

Merged
saddlepaddle merged 6 commits into
mainfrom
cli-workspace-terminal-cl
Jun 7, 2026
Merged

fix(host-service): make workspace delete reliably tear down terminal sessions#5168
saddlepaddle merged 6 commits into
mainfrom
cli-workspace-terminal-cl

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

A host with automated stale-workspace deletion accumulated ~400 live terminal sessions across already-deleted workspaces. Reported as "delete just removes the git worktree and leaves the sessions running."

This turned out to be a reliability bug, not a missing feature. The cascade already exists: CLI delete_workspace → host-service workspace.deletedestroyWorkspace, whose Step 2a calls disposeSessionsByWorkspaceId before removing the worktree. The problem is that the cascade could silently fail and orphan the PTY:

  1. disposeSessionAndWait marked the sqlite row disposed before confirming the daemon actually killed the PTY. A transient daemon failure left a live process behind a disposed row — invisible to every future workspace-scoped cleanup.
  2. The pty-daemon has no workspace mapping (SessionInfo is just {id,pid,cols,rows,alive}). The terminal→workspace link lives only in sqlite origin_workspace_id, and the onDelete: "set null" FK severs it once the workspace row is deleted — so an already-orphaned PTY can't be found by workspace at all.

Worktree removal reads git's durable on-disk registry so it always succeeds; terminal disposal trusted a mutable sqlite table and optimistically marked rows disposed — hence the asymmetry.

Changes

  • fix: mark disposed only after confirmed kill — failed kills now stay active and retryable/reapable instead of being lost behind a disposed row.
  • fix: clear dead terminal rows when destroying a workspace — the destroy saga deletes the workspace's confirmed-dead terminal rows (status ≠ active), so its session index dies with it instead of lingering as set null orphans. Failed kills (active) are deliberately kept reachable for the reaper.
  • feat: reap orphaned daemon PTYs — a reaper lists the daemon's live sessions and kills any whose row is disposed/exited/null-workspace (or row-less for two consecutive passes, so a being-born session — daemon PTY opened before the sqlite row insert — is never killed mid-creation). Runs at startup (drains the pre-existing ~400 on next host-service restart) and every 5 min; interval is unref()'d and cleared in dispose().

Why not onDelete: cascade?

An FK action fires at the DB level and can't know whether the PTY was actually killed. cascade would delete the row for a still-alive failed-kill PTY, turning it into a row-less orphan that's ambiguous with a being-born session (slower two-pass reap). set null leaves an unambiguous active+null row the reaper catches in one pass. Row lifecycle is therefore application-driven and conditional on a confirmed kill; the FK stays set null as a dumb integrity backstop.

Notes for deploy

  • The existing ~400 orphans drain on the next host-service restart (boot reaper) or within one 5-min tick after deploy.
  • Dev mode kills the daemon on exit, so this matters for production/detached daemons — the affected setup.

Verification

  • bun run typecheck
  • bun run lint exit 0 ✓
  • 352 host-service unit tests pass (bun 1.3.11)

Open in Stage

Summary by cubic

Ensure workspace deletion reliably tears down terminal sessions by fixing disposal semantics and adding a background reaper in host-service. The reaper now starts from serve after daemon bootstrap, avoids overlapping passes, and drains existing orphans on restart or within 5 minutes.

  • Bug Fixes

    • Mark a session disposed only after the daemon kill succeeds; failed kills stay active and retryable.
    • Reaper robustness: start from serve to avoid early daemon connections; skip overlapping passes, keep pending state per process, retry failed second‑pass rowless kills; include workspace id in the terminal-row cleanup warning.
    • On workspace destroy, delete that workspace’s non-active terminal rows to avoid orphaned records.
  • New Features

    • Add a terminal reaper that lists live daemon sessions and kills orphans (disposed/exited/null-workspace, or row-less on two passes). Runs on boot and every 5 minutes.

Written for commit cdb5085. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Background reaper now runs automatically to detect and clean orphaned terminal sessions.
  • Bug Fixes

    • Terminal sessions are marked disposed only when closure succeeds.
    • Workspace destroy now clears non-active terminal session records during local cleanup.
  • Chores

    • Improved error handling and non-blocking logging for cleanup operations.

…d kill

A failed daemon close left the sqlite row marked disposed while the PTY
stayed alive, so workspace-scoped cleanup could never find it again. Mark
the row disposed only once the kill is confirmed; failed kills stay active
and reapable.
Delete the workspace's confirmed-dead terminal rows in the destroy saga so
its session index dies with it instead of lingering as set-null orphans.
Still-active rows (failed kills) are kept reachable for the reaper.
The pty-daemon has no workspace mapping, so a PTY orphaned past workspace
deletion can only be recovered from the daemon's live-session list. Add a
reaper that kills daemon sessions whose row is dead/null-workspace, with a
two-pass guard so a being-born session is never killed mid-creation. Runs
at startup (drains pre-existing orphans) and every 5 minutes.
@coderabbitai

coderabbitai Bot commented Jun 6, 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89599e64-fc12-4970-b4cc-0f2b2f94c27a

📥 Commits

Reviewing files that changed from the base of the PR and between 895f214 and cdb5085.

📒 Files selected for processing (5)
  • packages/host-service/src/app.ts
  • packages/host-service/src/serve.ts
  • packages/host-service/src/terminal/reaper/index.ts
  • packages/host-service/src/terminal/reaper/reaper.ts
  • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts

📝 Walkthrough

Walkthrough

A background terminal session reaper periodically disposes daemon sessions not present in the host DB, DB row updates are applied only on successful daemon close, the reaper is started during app/server startup using the app DB, and workspace destroy now deletes non-active terminal session rows (best-effort).

Changes

Terminal Session Reaper & Cleanup

Layer / File(s) Summary
Conditional Session Disposal
packages/host-service/src/terminal/terminal.ts
disposeSessionAndWait now only updates terminalSessions (mark disposed/set endedAt) when the daemon close closeResult.succeeded is true.
Terminal Reaper Implementation
packages/host-service/src/terminal/reaper/reaper.ts, packages/host-service/src/terminal/reaper/index.ts
Adds ReapResult, reapOrphanedSessions(db, rowlessPendingSecondPass), and startTerminalReaper(db). The reaper lists daemon-alive sessions, loads DB rows, defers rowless sessions for a second pass, disposes orphans sequentially via disposeSessionAndWait, logs counts, runs immediately and on a 5-minute interval, and returns a cleanup function. index.ts re-exports startTerminalReaper.
App Lifecycle Integration
packages/host-service/src/app.ts, packages/host-service/src/serve.ts
CreateAppResult now exposes db: HostDb; createApp returns db. serve.ts imports startTerminalReaper, captures db from createApp(...), and calls startTerminalReaper(db) after server startup.
Workspace Cleanup Integration
packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts
During workspace destroy local cleanup (step 2a) the host now attempts to delete terminalSessions rows for the workspace where status != 'active'; failures are caught and appended to warnings. Drizzle imports updated to include and, ne, and terminalSessions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hop where orphaned sessions sigh,

I count the ghosts the daemon leaves behind,
I try once, then again, to tidy each tie,
DB and PTY now close in kind,
Hops and cleans — a neat host, peace of mind.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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
Title check ✅ Passed The title accurately describes the main change: fixing workspace deletion to reliably tear down terminal sessions through disposal semantics and a background reaper.
Description check ✅ Passed The description includes all required sections: problem context, changes made, deployment notes, and verification results. While the template itself is present, the author provided a comprehensive custom description covering the issue, solution, and rationale.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cli-workspace-terminal-cl

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 and usage tips.

@stage-review

stage-review Bot commented Jun 6, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 4 individual chapters for you:

Title
1 Mark sessions disposed only after confirmed kill
2 Clear dead terminal rows during workspace destruction
3 Implement background terminal session reaper
4 Wire database and reaper into application lifecycle
Open in Stage

Chapters generated by Stage for commit cdb5085 on Jun 7, 2026 12:07am UTC.

@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 `@packages/host-service/src/terminal/reaper/reaper.ts`:
- Around line 15-33: The reaper can start overlapping async passes
(startTerminalReaper -> run -> reapOrphanedSessions) causing races on
rowlessSessionsPendingSecondPass and duplicate disposeSessionAndWait calls; fix
by adding a run guard or serialized scheduling: inside startTerminalReaper add a
boolean flag (e.g., isRunning) or a simple mutex around run so that run returns
immediately if already running, set isRunning = true before calling
reapOrphanedSessions and clear it in finally, or replace setInterval with a
recursive setTimeout that schedules the next run only after the previous
reapOrphanedSessions completes; keep interval.unref()/timeout.unref() behavior
and ensure the initial immediate run is preserved.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 90179731-790d-4ad5-abe1-837b05086a63

📥 Commits

Reviewing files that changed from the base of the PR and between 045baaa and 895f214.

📒 Files selected for processing (5)
  • packages/host-service/src/app.ts
  • packages/host-service/src/terminal/reaper/index.ts
  • packages/host-service/src/terminal/reaper/reaper.ts
  • packages/host-service/src/terminal/terminal.ts
  • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts

Comment thread packages/host-service/src/terminal/reaper/reaper.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 5 files

Architecture diagram
sequenceDiagram
    participant CLI as CLI / API
    participant DB as SQLite DB
    participant Daemon as PTY Daemon
    participant Reaper as Terminal Reaper
    participant Host as Host Service

    Note over CLI,Host: Workspace Delete Flow

    CLI->>Host: delete_workspace(workspaceId)
    Host->>DB: disposeSessionsByWorkspaceId(workspaceId)
    Host->>Daemon: kill(terminalId)
    alt Daemon kill succeeds
        Daemon-->>Host: SUCCESS
        Host->>DB: mark status="disposed", endedAt=now
    else Daemon kill fails
        Daemon-->>Host: FAILURE
        Host->>Host: keep status="active" (retryable)
    end
    Host->>DB: delete non-active terminal rows for workspaceId
    Host->>Host: remove worktree
    Host-->>CLI: done

    Note over Host,Daemon: Background Reaper (boot + every 5 min)

    Reaper->>Daemon: list()
    Daemon-->>Reaper: [SessionInfo(id,pid,alive),...]
    Reaper->>DB: select id, status, originWorkspaceId from terminalSessions
    DB-->>Reaper: rows[]
    Reaper->>Reaper: identify orphans (disposed/exited/null-workspace/rowless x2)
    loop for each orphanId
        Reaper->>Daemon: kill(orphanId)
        alt kill succeeds
            Daemon-->>Reaper: SUCCESS
            Reaper->>DB: mark status="disposed"
        else kill fails
            Daemon-->>Reaper: FAILURE
            Reaper->>Reaper: keep status="active", log warning
        end
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/host-service/src/terminal/reaper/reaper.ts Outdated
Comment thread packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a reliability bug in workspace teardown where terminal PTY processes were orphaned after deletion — the cascade existed but the DB row was optimistically marked disposed before confirming the daemon actually killed the PTY, making failed kills invisible to all future cleanup. The fix also adds a startup/periodic reaper that cross-references the daemon's live session list against sqlite to recover pre-existing orphans.

  • terminal.ts: disposeSessionAndWait now writes status = \"disposed\" only after closeResult.succeeded, so failed kills stay active and remain visible to retries.
  • workspace-cleanup.ts: After the kill step, confirmed-dead rows (status != \"active\") for the workspace are deleted from sqlite so they don't accumulate as set null orphans after the workspace row is removed.
  • reaper.ts: A new reaper runs at startup and every 5 minutes, listing daemon live sessions, cross-referencing sqlite, and killing any whose row is disposed/exited/null-workspace; rowless sessions get a two-pass grace period to avoid killing sessions mid-creation.

Confidence Score: 4/5

Safe to merge — the core fix to disposeSessionAndWait and the workspace-cleanup delete step are sound, and the reaper handles all three orphan categories correctly.

The terminal fix and cleanup pipeline are well-reasoned and address the root cause cleanly. The two items worth a follow-up are both in the reaper: the module-level rowlessSessionsPendingSecondPass set makes tests order-dependent and creates a stale-state hazard if the reaper is ever restarted within a process, and failed-kill rowless orphans silently drop out of the pending set after each kill attempt, causing their retry interval to double to 10 minutes. Neither affects correctness in the production single-process scenario.

packages/host-service/src/terminal/reaper/reaper.ts warrants a second look for the shared-state and retry-cadence concerns; the other four files are straightforward.

Important Files Changed

Filename Overview
packages/host-service/src/terminal/reaper/reaper.ts New reaper module with module-level shared state (rowlessSessionsPendingSecondPass) that complicates isolation and causes suboptimal retry cadence for rowless orphans after a failed kill.
packages/host-service/src/terminal/terminal.ts Core fix: disposeSessionAndWait correctly defers the disposed DB write until after closeResult.succeeded, so failed kills stay active and reapable.
packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts New delete step correctly removes confirmed-dead terminal rows (ne(status, "active")) after the kill step, before the workspace FK triggers, preventing set null orphan accumulation.
packages/host-service/src/app.ts Wires startTerminalReaper into the app lifecycle with correct best-effort teardown pattern matching the existing cleanup structure.
packages/host-service/src/terminal/reaper/index.ts Thin barrel export for the reaper module; no issues.

Sequence Diagram

sequenceDiagram
    participant WC as workspace-cleanup
    participant TT as terminal.ts
    participant Daemon as PTY Daemon
    participant DB as SQLite
    participant Reaper as Terminal Reaper

    Note over WC: Step 2a - kill active PTYs
    WC->>TT: disposeSessionsByWorkspaceId(workspaceId)
    TT->>DB: "SELECT sessions WHERE workspace=X AND status!=disposed"
    DB-->>TT: [session rows]
    loop for each session
        TT->>Daemon: kill(sessionId)
        alt kill succeeds
            Daemon-->>TT: ok
            TT->>DB: "UPDATE status=disposed (NEW: only on success)"
        else kill fails
            Daemon-->>TT: error
            TT->>DB: no update - status stays active
        end
    end

    Note over WC: NEW Step - prune dead rows
    WC->>DB: "DELETE sessions WHERE workspace=X AND status!=active"

    Note over WC: Step 5 - delete workspace row
    WC->>DB: DELETE workspace row
    DB-->>DB: "FK onDelete set null - active rows get originWorkspaceId=null"

    Note over Reaper: Every 5 min and on startup
    Reaper->>Daemon: list() alive sessions
    Reaper->>DB: SELECT all terminal_sessions
    loop for each live session
        alt no DB row first pass
            Reaper->>Reaper: add to pendingSecondPass
        else no DB row second pass
            Reaper->>Daemon: kill(id)
        else "status=disposed OR exited OR originWorkspaceId=null"
            Reaper->>Daemon: kill(id)
            Daemon-->>Reaper: ok
            Reaper->>DB: "UPDATE status=disposed"
        end
    end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/host-service/src/terminal/reaper/reaper.ts:13
**Module-level mutable state leaks across reaper instances**

`rowlessSessionsPendingSecondPass` lives at module scope, so it persists across calls to `startTerminalReaper` (e.g., in unit tests or if the reaper is ever stopped and restarted). A second reaper started after a stop would inherit stale session IDs from the previous run: IDs that the new daemon knows nothing about will linger in the set until the cleanup loop at the top of the next `reapOrphanedSessions` call removes them. In tests that call `reapOrphanedSessions` directly, state from one test bleeds into the next, making tests order-dependent. Moving the Set inside `startTerminalReaper` (closing over it in `run`) would scope it to the instance lifetime.

### Issue 2 of 2
packages/host-service/src/terminal/reaper/reaper.ts:77-78
**Retry cadence for persistent rowless orphans doubles after a failed kill**

When a rowless orphan's kill fails in pass N (second-pass, so the ID was in `rowlessSessionsPendingSecondPass`), the ID is consumed by the `orphanIds` path and is **not** placed in `stillRowless`. The final `clear()` + re-population then leaves the ID out of the pending set entirely. On pass N+1 the session is seen as a brand-new rowless session and goes into `stillRowless`; the kill isn't retried until pass N+2 — doubling the effective retry interval to 10 minutes instead of 5. Re-adding the ID to `rowlessSessionsPendingSecondPass` after a failed kill (or keeping it in `stillRowless` when the kill failed) would restore the intended 5-minute cadence.

Reviews (1): Last reviewed commit: "feat(host-service): reap orphaned daemon..." | Re-trigger Greptile


const REAP_INTERVAL_MS = 5 * 60 * 1000;

const rowlessSessionsPendingSecondPass = new Set<string>();

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 Module-level mutable state leaks across reaper instances

rowlessSessionsPendingSecondPass lives at module scope, so it persists across calls to startTerminalReaper (e.g., in unit tests or if the reaper is ever stopped and restarted). A second reaper started after a stop would inherit stale session IDs from the previous run: IDs that the new daemon knows nothing about will linger in the set until the cleanup loop at the top of the next reapOrphanedSessions call removes them. In tests that call reapOrphanedSessions directly, state from one test bleeds into the next, making tests order-dependent. Moving the Set inside startTerminalReaper (closing over it in run) would scope it to the instance lifetime.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/terminal/reaper/reaper.ts
Line: 13

Comment:
**Module-level mutable state leaks across reaper instances**

`rowlessSessionsPendingSecondPass` lives at module scope, so it persists across calls to `startTerminalReaper` (e.g., in unit tests or if the reaper is ever stopped and restarted). A second reaper started after a stop would inherit stale session IDs from the previous run: IDs that the new daemon knows nothing about will linger in the set until the cleanup loop at the top of the next `reapOrphanedSessions` call removes them. In tests that call `reapOrphanedSessions` directly, state from one test bleeds into the next, making tests order-dependent. Moving the Set inside `startTerminalReaper` (closing over it in `run`) would scope it to the instance lifetime.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +77 to +78
rowlessSessionsPendingSecondPass.clear();
for (const id of stillRowless) rowlessSessionsPendingSecondPass.add(id);

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 Retry cadence for persistent rowless orphans doubles after a failed kill

When a rowless orphan's kill fails in pass N (second-pass, so the ID was in rowlessSessionsPendingSecondPass), the ID is consumed by the orphanIds path and is not placed in stillRowless. The final clear() + re-population then leaves the ID out of the pending set entirely. On pass N+1 the session is seen as a brand-new rowless session and goes into stillRowless; the kill isn't retried until pass N+2 — doubling the effective retry interval to 10 minutes instead of 5. Re-adding the ID to rowlessSessionsPendingSecondPass after a failed kill (or keeping it in stillRowless when the kill failed) would restore the intended 5-minute cadence.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/terminal/reaper/reaper.ts
Line: 77-78

Comment:
**Retry cadence for persistent rowless orphans doubles after a failed kill**

When a rowless orphan's kill fails in pass N (second-pass, so the ID was in `rowlessSessionsPendingSecondPass`), the ID is consumed by the `orphanIds` path and is **not** placed in `stillRowless`. The final `clear()` + re-population then leaves the ID out of the pending set entirely. On pass N+1 the session is seen as a brand-new rowless session and goes into `stillRowless`; the kill isn't retried until pass N+2 — doubling the effective retry interval to 10 minutes instead of 5. Re-adding the ID to `rowlessSessionsPendingSecondPass` after a failed kill (or keeping it in `stillRowless` when the kill failed) would restore the intended 5-minute cadence.

How can I resolve this? If you propose a fix, please make it concise.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

…eateApp

Starting the reaper inside createApp made it eagerly connect the pty-daemon
client at construction time, which raced integration tests that configure a
custom daemon socket after building the app (teardown terminals never spawned
-> timeouts). Move it to the serve listen callback, alongside connectRelay, so
it only runs in the real host process and after daemon bootstrap. Expose db on
CreateAppResult to wire it.
…state

- Skip a pass when one is already in flight, so setInterval can't interleave
  two passes racing on the pending-second-pass set.
- Move that set into the startTerminalReaper closure so it can't leak across
  instances/restarts.
- Re-queue a failed second-pass rowless kill so it retries on the next pass
  instead of restarting its two-pass clock.
@saddlepaddle

Copy link
Copy Markdown
Collaborator Author

Pushed fixes for the CI failure and the review findings:

CI failure (3 integration tests timing out) — root cause was the reaper being started inside createApp, which eagerly connected the pty-daemon client at construction. Those tests configure a custom daemon socket after building the app, so teardown terminals never spawned. Moved the reaper to the serve.ts listen callback (alongside connectRelay) so it only runs in the real host process and after daemon bootstrap; tests use createApp and are no longer affected. Full host-service suite now green locally (687 pass / 0 fail).

Overlapping passes (CodeRabbit + cubic) — added an in-flight guard so setInterval can't interleave two passes.

Module-level pending state (greptile) — moved rowlessPendingSecondPass into the startTerminalReaper closure; no longer shared across instances.

Retry cadence doubling (greptile) — a failed second-pass rowless kill is now re-queued into the pending set so it retries on the next 5-min pass instead of restarting its two-pass clock.

Warning context (cubic) — included workspaceId in the terminal-row cleanup warning.

@saddlepaddle saddlepaddle merged commit ce95dd5 into main Jun 7, 2026
17 checks passed
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