Skip to content

fix(host-service): detect ports for terminals without an attached renderer - #5325

Merged
Kitenite merged 4 commits into
mainfrom
polydactyl-platinum
Jun 29, 2026
Merged

Kitenite merged 4 commits into
mainfrom
polydactyl-platinum

Conversation

@Kitenite

@Kitenite Kitenite commented Jun 22, 2026 •

Copy link
Copy Markdown
Member

Problem

Ports were detected less completely in v2 than in v1. Both use the same shared @superset/port-scanner PortManager (2.5s periodic scan of registered terminal sessions, keyed by PID tree), so the gap was in what gets registered:

  • v1 (desktop daemon-manager.ts → reconcileOnStartup) pre-registers every alive daemon session with portManager.upsertSession(...) at startup, before any renderer attaches.
  • v2 host-service only called upsertSession inside createTerminalSessionInternal — i.e. only when a renderer creates or attaches to a terminal. Terminals the daemon is running but nobody has opened (notably sessions adopted after a host-service restart) were never scanned, so their ports never appeared in the v2 sidebar.

Fix

Fold a port-scan sync into the existing terminal reaper (reaper.ts) instead of adding a parallel module. The reaper already calls daemon.list() and joins terminalSessions (for terminalId → workspaceId) on the same pass, immediately at startup and on its interval — so there's no extra daemon call, DB query, or timer.

  • planPortScanSync() — pure decision function:
    • register every alive daemon session that maps to an active workspace row and isn't already owned by a live in-memory session;
    • unregister every watched terminal the daemon no longer reports and no live in-memory session owns (adopted sessions never get the daemon exit subscription that normally unregisters them).
  • applyPortScanSync() applies the plan to portManager on each reaper pass, before the empty-list short-circuit so an empty daemon clears stale scans.
  • Supporting helpers: PortManager.getRegisteredTerminalIds() and terminal.ts isLiveTerminalSession() (the latter guards a renderer-attached session against being dropped during a racy daemon.list()).

The terminalId → workspaceId join is irreducible: daemon.list() returns {id, pid, alive} only, and the sidebar groups ports by workspace — that mapping lives in the host DB, which the reaper already reads.

End-to-end

  • On start: eventBus.start() (in createApp) wires portManager → port:changed broadcast; the reaper's immediate first pass registers live sessions → periodic scan begins. Bridge is wired before registration, so no early events drop.
  • Renderer reads: ports.getAll returns portManager.getAllPorts(), now populated for unattached terminals.
  • Renderer subscribes: the event-bus port:changed stream delivers live add/remove for all live terminals, attached or not.

Notes

  • Self-heal cleanup of adopted-but-exited registrations rides the reaper's 5-min cadence; their ports still vanish within 2.5s via the empty-tree scan, only the no-op registration lingers. Attached terminals unregister instantly on the daemon exit event.

Testing

  • New reaper.test.ts covers planPortScanSync (register/skip/unregister/empty-daemon/race-guard); verified each case fails under impl mutation.
  • bun run lint exits 0; typecheck passes for @superset/host-service and @superset/port-scanner; existing port-scanner tests pass.

Summary by cubic

Fixes missing port detection for terminals running without an attached renderer by syncing the port scanner with daemon sessions during reaper passes. Ports now appear for adopted/live terminals right after start and stay accurate.

  • Bug Fixes
    • Sync portManager with alive daemon sessions on each reaper pass: register mapped active rows not owned by a live in-memory session; unregister watched terminals the daemon no longer reports; guard renderer-attached sessions via isLiveTerminalSession.
    • Skip the DB join when the daemon reports no live sessions while still dropping stale scans; isolate the sync in try/catch so reaper cleanup can’t be blocked.
    • Added PortManager.getRegisteredTerminalIds(), planPortScanSync, and tests.

Written for commit 9c940ac. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Improved port scan management by reconciling with the daemon’s active terminal sessions: newly live terminals are registered, stale ones are unregistered, and previously adopted scans are cleared when no live sessions are reported.
    • Added a helper to treat only non-exited in-memory terminal sessions as “live” for reconciliation.
    • Exposed a method to retrieve currently registered terminal IDs for port scanning.
  • Tests

    • Added Bun coverage for port-scan synchronization scenarios, including adoption, skipping owned sessions, handling missing/inactive rows, unregistering removed terminals, clearing when none are live, and temporary daemon absence.

…ched renderer

v2 registered a terminal for port scanning only when a renderer attached
to it, so dev servers in unopened terminals — notably sessions the daemon
keeps alive across a host-service restart — were never scanned. v1 desktop
avoids this by registering all live daemon sessions at startup.

Fold a port-scan sync into the existing terminal reaper, which already
lists daemon sessions and joins terminalSessions for workspace ids on the
same pass (immediately at startup + on its interval). It registers alive
sessions mapped to an active workspace row and not already owned by a live
in-memory session, and unregisters watched terminals the daemon no longer
reports. The existing event-bus bridge (portManager -> port:changed) and
ports.getAll already fan these out to the renderer.
@coderabbitai

coderabbitai Bot commented Jun 22, 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: 61dcc3ac-5593-41be-8c7b-f62dd7d6f165

📥 Commits

Reviewing files that changed from the base of the PR and between 7c5a6e4 and 9c940ac.

📒 Files selected for processing (1)
  • packages/host-service/src/terminal/reaper/reaper.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/host-service/src/terminal/reaper/reaper.ts

📝 Walkthrough

Walkthrough

Adds port-scan reconciliation to the terminal reaper. A new pure planPortScanSync function computes which terminal sessions to register or unregister for port scanning, based on daemon live sessions, DB rows, in-memory liveness, and existing registrations. applyPortScanSync wires this plan to portManager, and reapOrphanedSessions calls it before its empty-daemon early exit. Supporting primitives getRegisteredTerminalIds and isLiveTerminalSession are added.

Changes

Port-scan reconciliation

Layer / File(s) Summary
Supporting primitives: getRegisteredTerminalIds and isLiveTerminalSession
packages/port-scanner/src/port-manager.ts, packages/host-service/src/terminal/terminal.ts
PortManager.getRegisteredTerminalIds() returns current registered terminal IDs from the internal sessions map. isLiveTerminalSession(terminalId) checks that a terminal session exists in-memory and has not exited.
PortScanSyncPlan, planPortScanSync, applyPortScanSync, and reapOrphanedSessions wiring
packages/host-service/src/terminal/reaper/reaper.ts
Imports portManager and isLiveTerminalSession. Defines PortScanSyncPlan and exported planPortScanSync computing register/unregister sets from daemon sessions, DB rows (originWorkspaceId + status === "active"), registered IDs, and an isLive guard. applyPortScanSync drives portManager.upsertSession and portManager.unregisterSession from the plan. reapOrphanedSessions calls applyPortScanSync before the empty-daemon early return so stale scans are cleared.
planPortScanSync test suite
packages/host-service/src/terminal/reaper/reaper.test.ts
Seven Bun tests covering: register on active row, skip when already live in-memory, skip on missing row/workspace/inactive status, unregister stale terminals, clear all on empty daemon list, and retain renderer-attached sessions temporarily absent from daemon.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • superset-sh/superset#5168: Both PRs modify the host-service terminal reaper flow in reaper.ts, including the handling of daemon live sessions during reapOrphanedSessions.

Poem

🐇 Hop, hop, the daemon glows,
Live sessions in their tidy rows.
Register here, unregister ત્યાં? no,
The reaper keeps the port-scan flow.
When silence comes, it clears the trail —
A little hop to set things pale!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 clearly states the core fix: detecting ports for terminals without an attached renderer.
Description check ✅ Passed The description is detailed and covers problem, fix, behavior, notes, and testing, with only some template sections missing.
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.
✨ 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 polydactyl-platinum

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.

@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 and verified against the latest diff

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

Re-trigger cubic

@github-actions

github-actions Bot commented Jun 22, 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! 🎉

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a port-detection gap in v2 host-service: terminals running after a host restart (not yet attached to any renderer) were never registered with the PortManager, so their dev-server ports didn't appear in the sidebar. The fix folds a port-scan reconciliation step into the existing reapOrphanedSessions loop — same daemon RPC, same DB read, no extra timer.

  • planPortScanSync (pure, tested) computes which daemon sessions to register (alive, active workspace, not already in-memory) and which stale registrations to drop (no longer in daemon.list(), no in-memory owner); an isLive guard prevents a racy daemon.list() from evicting a renderer-attached session.
  • applyPortScanSync wires the plan to portManager.upsertSession / portManager.unregisterSession on every reaper pass, placed before the empty-list short-circuit so a fully idle daemon still clears stale scans.
  • PortManager.getRegisteredTerminalIds() and isLiveTerminalSession() are small supporting additions to expose the state each side needs for the reconciliation.

Confidence Score: 4/5

Safe to merge. The reconciliation logic is correct for all adoption, race-guard, and cleanup scenarios, and is well-covered by the new unit tests.

The core planPortScanSync function correctly handles all edge cases: it avoids double-registration of live sessions, clears stale scans when the daemon is empty, and guards against a racy daemon.list() drop of renderer-attached sessions. Two minor inefficiencies exist — the DB query running unconditionally even when liveSessions is empty, and already-adopted sessions being re-upserted on every pass — but neither affects correctness since upsertSession is idempotent and the query is lightweight. Tests cover all six key branches.

reaper.ts deserves a second look for the unconditional DB query when liveSessions is empty (rowById is never read in that path).

Important Files Changed

Filename Overview
packages/host-service/src/terminal/reaper/reaper.ts Core of the fix: adds planPortScanSync (pure, exported for testing), applyPortScanSync, and wires them into the reapOrphanedSessions loop. The DB query now runs unconditionally even when liveSessions is empty (rowById is unused in that path), but this is a minor efficiency concern. Logic is correct for all cases described in the PR.
packages/host-service/src/terminal/reaper/reaper.test.ts New test file with 6 focused unit tests for planPortScanSync covering all key branches: register, skip-live, skip-inactive/no-workspace, unregister stale, empty daemon clear, and race-guard for attached sessions.
packages/host-service/src/terminal/terminal.ts Adds isLiveTerminalSession(): checks the module-level sessions map for a non-exited session. Simple and correct; correctly returns false for sessions that exist but have exited.
packages/port-scanner/src/port-manager.ts Adds getRegisteredTerminalIds(): returns Array.from(this.sessions.keys()), a snapshot copy — safe to iterate while the map is modified. Minimal, correct addition.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant R as TerminalReaper
    participant D as DaemonClient
    participant DB as HostDb
    participant P as PortManager
    participant T as terminal.ts

    R->>D: daemon.list()
    D-->>R: sessions (id, pid, alive)
    R->>R: "filter alive = liveSessions"
    R->>DB: SELECT id, status, originWorkspaceId
    DB-->>R: rows
    R->>R: build rowById Map
    R->>T: isLive(terminalId) for each liveSession
    T-->>R: false if no in-memory session
    R->>P: upsertSession(terminalId, workspaceId, pid)
    Note over R,P: register adopted sessions without in-memory owner
    R->>T: isLive(registeredId) for stale scans
    T-->>R: false if no in-memory session
    R->>P: unregisterSession(terminalId)
    Note over R,P: drop sessions absent from daemon.list and not live
    alt liveSessions is empty
        R->>R: clear rowlessPendingSecondPass and return
    else has live sessions
        R->>R: orphan detection and disposeSessionAndWait
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant R as TerminalReaper
    participant D as DaemonClient
    participant DB as HostDb
    participant P as PortManager
    participant T as terminal.ts

    R->>D: daemon.list()
    D-->>R: sessions (id, pid, alive)
    R->>R: "filter alive = liveSessions"
    R->>DB: SELECT id, status, originWorkspaceId
    DB-->>R: rows
    R->>R: build rowById Map
    R->>T: isLive(terminalId) for each liveSession
    T-->>R: false if no in-memory session
    R->>P: upsertSession(terminalId, workspaceId, pid)
    Note over R,P: register adopted sessions without in-memory owner
    R->>T: isLive(registeredId) for stale scans
    T-->>R: false if no in-memory session
    R->>P: unregisterSession(terminalId)
    Note over R,P: drop sessions absent from daemon.list and not live
    alt liveSessions is empty
        R->>R: clear rowlessPendingSecondPass and return
    else has live sessions
        R->>R: orphan detection and disposeSessionAndWait
    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:105-119
The full `terminalSessions` SELECT now runs even when `liveSessions` is empty. In that code path `rowById` is never accessed — the `register` loop iterates over `liveSessions` (empty), so the query result is immediately discarded. Before this PR the DB call was guarded by the early-return. Moving the query unconditionally before `applyPortScanSync` is fine for correctness, but the query can be skipped when there are no live sessions because `planPortScanSync` only reads `rowById` inside the register loop.

```suggestion
	// Keep the port scanner in sync with the daemon's live sessions on the same
	// pass that lists them — registers ports for terminals running without an
	// attached renderer, and drops scans for ones the daemon has dropped. Runs
	// before the empty-list short-circuit so an empty daemon clears stale scans.
	// rowById is only consulted by the register path, so skip the query when
	// there are no live sessions to register.
	const rowById: Map<string, TerminalRow> =
		liveSessions.length > 0
			? new Map(
					db
						.select({
							id: terminalSessions.id,
							status: terminalSessions.status,
							originWorkspaceId: terminalSessions.originWorkspaceId,
						})
						.from(terminalSessions)
						.all()
						.map((row) => [row.id, row]),
				)
			: new Map();
	applyPortScanSync(liveSessions, rowById);
```

### Issue 2 of 2
packages/host-service/src/terminal/reaper/reaper.ts:57-68
**Register path re-upserts already-adopted sessions every pass**

The `register` loop never consults `registeredTerminalIds`, so every non-live, active, workspace-mapped daemon session is passed to `portManager.upsertSession` on every reaper invocation (every 5 minutes), regardless of whether it was already registered in a prior pass. `upsertSession` is idempotent (`sessions.set` with the same values), so there is no functional bug. It's worth noting in case a future change makes `upsertSession` non-idempotent (e.g. resetting a `firstSeenAt` timestamp or triggering an eager scan), at which point a guard like `if (!registeredTerminalIds.includes(session.id))` in `planPortScanSync` would be needed.

Reviews (1): Last reviewed commit: "refactor(host-service): trim redundant p..." | Re-trigger Greptile

Comment on lines +105 to +119
@@ -31,6 +112,15 @@ async function reapOrphanedSessions(
.all();
const rowById = new Map(rows.map((row) => [row.id, row]));

// Reuse the list + row join the reaper already did. Must run before the
// empty-list short-circuit so an empty daemon still clears stale scans.
applyPortScanSync(liveSessions, rowById);

if (liveSessions.length === 0) {

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 The full terminalSessions SELECT now runs even when liveSessions is empty. In that code path rowById is never accessed — the register loop iterates over liveSessions (empty), so the query result is immediately discarded. Before this PR the DB call was guarded by the early-return. Moving the query unconditionally before applyPortScanSync is fine for correctness, but the query can be skipped when there are no live sessions because planPortScanSync only reads rowById inside the register loop.

Suggested change
// Keep the port scanner in sync with the daemon's live sessions on the same
// pass that lists them — registers ports for terminals running without an
// attached renderer, and drops scans for ones the daemon has dropped. Runs
// before the empty-list short-circuit so an empty daemon clears stale scans.
// rowById is only consulted by the register path, so skip the query when
// there are no live sessions to register.
const rowById: Map<string, TerminalRow> =
liveSessions.length > 0
? new Map(
db
.select({
id: terminalSessions.id,
status: terminalSessions.status,
originWorkspaceId: terminalSessions.originWorkspaceId,
})
.from(terminalSessions)
.all()
.map((row) => [row.id, row]),
)
: new Map();
applyPortScanSync(liveSessions, rowById);
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/terminal/reaper/reaper.ts
Line: 105-119

Comment:
The full `terminalSessions` SELECT now runs even when `liveSessions` is empty. In that code path `rowById` is never accessed — the `register` loop iterates over `liveSessions` (empty), so the query result is immediately discarded. Before this PR the DB call was guarded by the early-return. Moving the query unconditionally before `applyPortScanSync` is fine for correctness, but the query can be skipped when there are no live sessions because `planPortScanSync` only reads `rowById` inside the register loop.

```suggestion
	// Keep the port scanner in sync with the daemon's live sessions on the same
	// pass that lists them — registers ports for terminals running without an
	// attached renderer, and drops scans for ones the daemon has dropped. Runs
	// before the empty-list short-circuit so an empty daemon clears stale scans.
	// rowById is only consulted by the register path, so skip the query when
	// there are no live sessions to register.
	const rowById: Map<string, TerminalRow> =
		liveSessions.length > 0
			? new Map(
					db
						.select({
							id: terminalSessions.id,
							status: terminalSessions.status,
							originWorkspaceId: terminalSessions.originWorkspaceId,
						})
						.from(terminalSessions)
						.all()
						.map((row) => [row.id, row]),
				)
			: new Map();
	applyPortScanSync(liveSessions, rowById);
```

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

Comment on lines +57 to +68
const register: PortScanSyncPlan["register"] = [];
for (const session of liveSessions) {
if (isLive(session.id)) continue;
const row = rowById.get(session.id);
if (!row?.originWorkspaceId) continue;
if (row.status !== "active") continue;
register.push({
terminalId: session.id,
workspaceId: row.originWorkspaceId,
pid: session.pid,
});
}

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 Register path re-upserts already-adopted sessions every pass

The register loop never consults registeredTerminalIds, so every non-live, active, workspace-mapped daemon session is passed to portManager.upsertSession on every reaper invocation (every 5 minutes), regardless of whether it was already registered in a prior pass. upsertSession is idempotent (sessions.set with the same values), so there is no functional bug. It's worth noting in case a future change makes upsertSession non-idempotent (e.g. resetting a firstSeenAt timestamp or triggering an eager scan), at which point a guard like if (!registeredTerminalIds.includes(session.id)) in planPortScanSync would be needed.

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

Comment:
**Register path re-upserts already-adopted sessions every pass**

The `register` loop never consults `registeredTerminalIds`, so every non-live, active, workspace-mapped daemon session is passed to `portManager.upsertSession` on every reaper invocation (every 5 minutes), regardless of whether it was already registered in a prior pass. `upsertSession` is idempotent (`sessions.set` with the same values), so there is no functional bug. It's worth noting in case a future change makes `upsertSession` non-idempotent (e.g. resetting a `firstSeenAt` timestamp or triggering an eager scan), at which point a guard like `if (!registeredTerminalIds.includes(session.id))` in `planPortScanSync` would be needed.

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

- skip the terminalSessions query when the daemon reports no live sessions
  (the row join is only consulted to register live sessions; an empty
  daemon still runs the sync to drop stale scans, just with an empty map)
- isolate applyPortScanSync in try/catch so a port-manager error can't
  abort the reaper's orphan cleanup for that interval
@Kitenite

Copy link
Copy Markdown
Member Author

Both addressed in 7c5a6e4:

  1. Unconditional DB select — rowById is now built via loadTerminalRowsById(db) only when liveSessions.length > 0; the empty-daemon path passes new Map(), so an idle host still runs the sync to clear stale scans but does no DB query.
  2. try/catch isolation — applyPortScanSync now wraps its body so a port-manager error logs and returns instead of aborting the reaper's orphan cleanup for the interval.

@ghost

ghost commented Jun 24, 2026 •

Copy link
Copy Markdown

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

Title
1 Expose registered terminal IDs from PortManager
2 Add terminal session liveness check
3 Implement port scan synchronization logic
4 Test port scan synchronization planning
Open in Stage

Chapters generated by Stage for commit 9c940ac on Jun 29, 2026 3:14am UTC.

@Kitenite
Kitenite merged commit 8c05512 into main Jun 29, 2026
17 checks passed
@Kitenite
Kitenite deleted the polydactyl-platinum branch June 29, 2026 14:49
Kitenite added a commit that referenced this pull request Jul 3, 2026
After a host-service restart the detached pty-daemon keeps dev servers alive,
but the new host-service starts with an empty port scanner. #5325 re-registers
those adopted sessions via the terminal reaper — but the reaper runs its
port-scan sync only once immediately and then every 5 minutes, and the
just-adopted daemon may not yet list its sessions the instant that first pass
runs. So ports stayed missing until the 5-minute tick or until the user selected
the workspace (which attaches a renderer and registers the terminal directly).

Decouple the port-scan sync from the 5-minute orphan-reap cadence and run it a
few times over the first ~90s after startup (2/5/10/20/45/90s). This re-registers
restored dev-server ports promptly without disturbing the reaper's two-pass
rowless-orphan clock. Also log when the sync registers unattached sessions so the
recovery is observable in the field.

- Extract syncPortScans() (daemon.list + row join + applyPortScanSync); the reap
  pass reuses its result, so no extra daemon.list call there.
- Warm-up offsets all stay below REAP_INTERVAL_MS; covered by a schedule test.
Kitenite added a commit that referenced this pull request Jul 3, 2026
…rt (#5430)

* fix(host-service): recover v2 ports promptly after host-service restart

After a host-service restart the detached pty-daemon keeps dev servers alive,
but the new host-service starts with an empty port scanner. #5325 re-registers
those adopted sessions via the terminal reaper — but the reaper runs its
port-scan sync only once immediately and then every 5 minutes, and the
just-adopted daemon may not yet list its sessions the instant that first pass
runs. So ports stayed missing until the 5-minute tick or until the user selected
the workspace (which attaches a renderer and registers the terminal directly).

Decouple the port-scan sync from the 5-minute orphan-reap cadence and run it a
few times over the first ~90s after startup (2/5/10/20/45/90s). This re-registers
restored dev-server ports promptly without disturbing the reaper's two-pass
rowless-orphan clock. Also log when the sync registers unattached sessions so the
recovery is observable in the field.

- Extract syncPortScans() (daemon.list + row join + applyPortScanSync); the reap
  pass reuses its result, so no extra daemon.list call there.
- Warm-up offsets all stay below REAP_INTERVAL_MS; covered by a schedule test.

* fix(host-service): coalesce concurrent port-scan syncs

Address review: the warm-up timers and the initial reap pass both call
syncPortScans unguarded. A slow daemon.list right after adoption (exactly when
the warm-up fires) could let a second sync observe a transiently-empty list and
unregister sessions the first just registered. Coalesce concurrent callers onto
one in-flight run.

* docs(host-service): fix stale port-scan-sync comment after warm-up refactor
Kitenite added a commit that referenced this pull request Jul 3, 2026
…ntry (#5438)

* fix(desktop): start the terminal reaper in the desktop host-service entry

startTerminalReaper was only wired into packages/host-service serve.ts
(the standalone/CLI entry), so the packaged desktop app never ran the
orphan reap (#5168) or the port-scan reconcile for daemon-adopted
sessions (#5325/#5430). Ports for terminals that survived a host-service
restart stayed invisible until the workspace was selected.

* refactor(host-service): start the terminal reaper inside createApp

Both real entry points (serve.ts and the desktop entry) consume
createApp, so owning the reaper there removes the per-entry wiring that
let the desktop app ship without it. Tests opt out via createTestHost:
the reaper's warm-up timers reach the real pty-daemon through the
process-wide daemon-client singleton and reap other tests' sessions
(observed as 5 integration failures with an unconditional start). The
reaper's stop function joins createApp's dispose chain.

* refactor(host-service): move the reaper behind a package-owned startHostRuntime

createApp goes back to building the request-serving surface only — no
reaper and no test-only escape hatch. Process-lifetime background work
now lives in startHostRuntime (runtime/host-runtime.ts), which both
serving entries call once after binding. Tests consume createApp and
are untouched.

* refactor(host-service): drop the startHostRuntime pass-through

The wrapper only delegated to startTerminalReaper. Entries call the
reaper directly, same as installProcessSafetyNet and connectRelay —
one invocation per serving entry, logic stays in the package.

* chore(desktop): tighten the reaper comment

* chore(desktop): drop call-site comment on startTerminalReaper

* chore(desktop): one-line comment on startTerminalReaper

This branch was previously deployed

1 inactive deployment
preview — 9c940ac3 Deployed Jun 29, 2026 by Kitenite via Deploy Admin #6042
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