Skip to content

fix(daemon): retire empty current-generation daemons#9277

Merged
OrcaWin merged 4 commits into
mainfrom
Jinwoo-H/investigate-issue-9138
Jul 20, 2026
Merged

fix(daemon): retire empty current-generation daemons#9277
OrcaWin merged 4 commits into
mainfrom
Jinwoo-H/investigate-issue-9138

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

ELI5

Orca intentionally leaves terminal daemons alive when the app disconnects so live terminals can
reattach after an update or crash. The missing piece was that a new daemon with no terminals also
stayed alive forever.

This change lets a new v24 daemon retire itself only when it can atomically prove it has no terminal,
no other client, no unknown connection, and no terminal creation in progress. Live terminals keep
the existing survival behavior.

Before / after

Before

app disconnects -> daemon stays forever
                    |- live sessions stay (wanted)
                    `- zero sessions also stay (leak)

After

clean detach -> daemon checks itself atomically
                |- any session/work/client -> stay alive
                `- exactly empty -> exit immediately

unexpected drop -> daemon checks itself atomically
                   |- any session/work/connection -> stay alive
                   `- exactly empty -> exit immediately

v23 and older -> unchanged and reattachable

Root cause

Daemon endpoints are versioned by protocol, so upgrades can create a new generation while retaining
older generations for live PTY reattachment. The daemon had no authenticated, race-safe empty
retirement operation, and app startup did not have enough evidence to safely kill a process.

What changes

  • Bumps the lifecycle contract to protocol v24. main assigned v23 to the macOS login-shell change
    while this work was in progress, so v23 remains a legacy generation.
  • Gives v24 hello responses an exact endpoint identity: PID, self-reported process start time, and a
    per-launch nonce. Both authenticated sockets must report the same identity.
  • Adds daemon-owned shutdownIfIdle. It synchronously closes admission before acknowledging only
    when the authenticated requester is the sole complete client, every transport is accounted for,
    no create/attach is in flight, and there are zero sessions.
  • Sends that operation on clean provider detach with one 250 ms connect-plus-RPC budget.
  • Makes authenticated unexpected disconnect event-driven: live sessions, clients, in-flight work,
    and unknown transports block retirement; an exactly empty daemon exits immediately.
  • Keeps a two-minute watchdog only during the initial launch-to-first-adoption handoff. A complete
    control+stream adoption cancels it permanently, so it is never a terminal inactivity timeout.
  • Requires a complete authenticated control+stream pair before terminal admission, so a partial
    replacement can block retirement but cannot erase its evidence.
  • Updates the Windows built-daemon workspace-close repro to use that same production-faithful pair,
    with bounded control/stream handshakes and deterministic failure cleanup.
  • Establishes a temporary full startup pair and overlaps it with the permanent adapter pair, so
    there is no retirement gap while a relaunched app adopts the daemon.
  • Makes token/PID cleanup replacement-safe and limits token-missing respawn to clients that first
    observed an authenticated disconnect.
  • Keeps normal connection timeout behavior per socket/hello while bounded quit callers and waiters
    retain an explicit total budget and generation fence against late socket resurrection.

There is no app-startup kill, no legacy-daemon retirement, and no inference from worktree/layout
absence.

Scope split and archived work

The larger ownership persistence, cross-profile audit, candidate journal, transfer recovery, and
enforcement prototype has been removed from this PR. It remains recoverable at:

The narrowed design incorporates the safety constraints from AmethystLiang's reviewed design:
#9138 (comment)

Performance

This PR adds no polling, process scan, profile enumeration, ownership checksum, candidate journal,
per-byte hashing, or steady-state persistence write. Lifecycle bookkeeping runs on connection,
disconnection, terminal admission/exit, and quit events. Startup adds one authenticated two-socket
lease and one unref'ed pre-adoption watchdog; clean quit adds one bounded RPC. There is no runtime
disconnect timer after adoption.

The local machine was too busy for publication-grade absolute benchmarks (load averages 17-38 with
unrelated browser, simulator, VM, and Orca work). A paired same-host regression screen bracketed five
main v23 samples with ten v24 branch samples:

Median main v23 branch v24
two-socket connect 1.38 ms 1.32 ms
listSessions RPC 0.0366 ms 0.0374 ms
terminal echo stream 3.28 MiB/s 3.14 MiB/s

All medians were within about 5%, but individual throughput/event-loop samples varied widely under
host load. Treat this as evidence against a large regression, not a precise delta.

Validation

  • Three independent review tracks iterated until all returned clean, covering architecture,
    ownership safety, connection/respawn races, process termination, and test fidelity.
  • Full daemon suite after the final rebase: 56 files passed, 2 skipped; 939 tests passed, 5 skipped.
  • Focused post-review suite: 3 files and 74 tests passed.
  • Windows harness workflow contract: 12 tests passed; its pair-authentication update completed two
    independent review tracks after timeout and rejection-path findings were fixed.
  • Process E2E: v22 remained live and reattachable while the exact empty v24 process exited immediately
    after its final authenticated client disconnected and removed only its owned PID/token/socket
    artifacts.
  • Full typecheck, focused oxlint, max-lines ratchet, and git diff --check passed.
  • Full Electron main/preload/renderer E2E build passed.
  • All required CI passed on cdc89e1294: repository verify, packaged Windows crash-survival,
    Windows terminal restart regressions, and Ubuntu/Windows native smoke including the built-daemon
    ConPTY workspace-close repro.

The local shell used Node 26.5.0 while the repository requests Node 24. Existing Vite chunk/CSS,
Swift deprecation, and CLI symlink warnings remained; the build completed successfully. CI provides
the supported Node and platform matrix.

Tradeoffs

  • An unexpected empty disconnect now retires immediately. A transient socket failure may therefore
    relaunch an empty daemon instead of reusing it, adding bounded reconnect/startup latency but never
    terminating a live session.
  • v23 and older daemons remain untouched because they cannot prove the new exact identity and atomic
    retirement contract. This PR prevents new v24 empty-generation buildup but does not clean existing
    legacy generations.
  • A raw or partial connection pauses retirement rather than risking a false shutdown.
  • Rolling back makes v24 another preserved legacy generation; an older app cannot shut it down.

Screenshots

No visual change.

AI Review Report

Three independent reviewers completed approximately 18 review loops until clean. Valid findings were
fixed and regression-tested, including shutdown reply flushing, startup child cleanup, adoption
lease handoff/disposal races, second-client preservation, monotonic retirement across partial
clients, complete-pair-only terminal admission, exact PID-record cleanup, launcher ownership-pair
validation, and E2E teardown.

Security Audit

  • No token contents or terminal data are newly logged or exposed.
  • Both sockets must authenticate and report the same endpoint identity before the client treats them
    as one daemon connection.
  • Cleanup only removes exact PID/token artifacts owned by the matching daemon incarnation.
  • Daemon retirement fails closed when clients, sessions, in-flight admission, or unknown transports
    make emptiness uncertain.

Notes

The broader ownership-audit prototype remains archived at the branch and commit above; it is not
part of this PR. Full repository lint still stops on the pre-existing unrelated exhaustiveness error
in src/renderer/src/components/skills/skill-freshness-group.tsx:101; all changed files pass focused
lint.

Refs #9138

Made with Orca 🐋

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change introduces protocol v24 daemon endpoint identities and launch-scoped PID records, including strict readiness validation and ownership-safe cleanup. It adds timeout-aware client connections, identity matching across sockets, clean-disconnect retirement, idle shutdown fencing, respawn leases, and legacy protocol compatibility. Daemon launch, initialization, adapter retry, server shutdown, CLI parsing, unit tests, and end-to-end lifecycle fixtures are updated accordingly.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.40% 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.
Title check ✅ Passed The title concisely and accurately summarizes the main change: retiring empty current-generation daemons.
Description check ✅ Passed The description covers the required content in substance, with summary, screenshots, testing, review, security, and notes all addressed.
✨ Finishing Touches
📝 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.

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

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (2)
src/main/daemon/daemon-health.ts (1)

320-329: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail closed on malformed process identity values.

pid, startedAtMs, and launchNonce are less strictly validated than endpoint identities. For example, a non-positive start time can become a start-time mismatch and classify a matching live command/nonce as absent, permitting cleanup and replacement. Require a positive safe PID/start time and non-empty nonce, or classify the identity as unknown.

Proposed fix
-if (typeof parsed.pid === 'number' && Number.isFinite(parsed.pid)) {
+if (Number.isSafeInteger(parsed.pid) && (parsed.pid as number) > 0) {
   return {
     pid: parsed.pid,
     startedAtMs:
       typeof parsed.startedAtMs === 'number' &&
-      Number.isFinite(parsed.startedAtMs)
+      Number.isFinite(parsed.startedAtMs) &&
+      parsed.startedAtMs > 0
         ? parsed.startedAtMs
         : null,
     entryPath: typeof parsed.entryPath === 'string' ? parsed.entryPath : null,
     appVersion: typeof parsed.appVersion === 'string' ? parsed.appVersion : null,
-    ...(typeof parsed.launchNonce === 'string' ? { launchNonce: parsed.launchNonce } : {})
+    ...(typeof parsed.launchNonce === 'string' && parsed.launchNonce.length > 0
+      ? { launchNonce: parsed.launchNonce }
+      : {})
   }
 }
-if (args.expectedStartedAtMs === null || !args.launchNonce) {
+if (
+  args.expectedStartedAtMs === null ||
+  !Number.isFinite(args.expectedStartedAtMs) ||
+  args.expectedStartedAtMs <= 0 ||
+  !args.launchNonce
+) {
   return 'unknown'
 }

Also applies to: 506-523

src/main/daemon/daemon-init.ts (1)

1184-1206: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not report nonexistent legacy generations as failures.

When both socket and PID file are absent, readFileSync throws ENOENT, legacyDaemonProcessState() returns unknown, and the protocol is added to failedProtocols. A clean install therefore marks protocols 1–22 failed, keeping reconciliation and generation audit incomplete. Skip protocols with no endpoint artifacts; reserve failedProtocols for actual unverifiable candidates.

🟠 Major comments (25)
tests/e2e/helpers/daemon-generation-processes.ts-5-5 (1)

5-5: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not accept adjacent process starts as the same incarnation.

A PID reused within the 1.1–2 second tolerance is treated as the recorded process and may then be killed by cleanup. These comparisons use the same OS-derived creation timestamp, so compare it exactly; if its resolution is insufficient, use a stronger platform identity rather than widening the match.

Minimal safety fix
-const PROCESS_IDENTITY_TOLERANCE_MS = process.platform === 'win32' ? 2_000 : 1_100
-    Math.abs(current.startedAtMs - identity.startedAtMs) <= PROCESS_IDENTITY_TOLERANCE_MS
+    current.startedAtMs === identity.startedAtMs
-    Math.abs(currentRoot.startedAtMs - root.startedAtMs) > PROCESS_IDENTITY_TOLERANCE_MS
+    currentRoot.startedAtMs !== root.startedAtMs

Also applies to: 107-113, 149-153

tests/e2e/helpers/daemon-generation-fixtures.ts-273-283 (1)

273-283: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not replace the v23 daemon’s owned PID record.

When launchNonce exists, the PID path was already supplied to the daemon. Overwriting it after readiness can race idle cleanup, recreate a stale artifact after removal, and validates synthetic data instead of the production writer. Only synthesize records for legacy fixtures.

Proposed fix
-    writeFileSync(
-      pidPath,
-      `${JSON.stringify({
-        pid: daemonIdentity.pid,
-        startedAtMs: daemonIdentity.startedAtMs,
-        entryPath,
-        fixtureNonce: randomUUID(),
-        ...(launchNonce ? { launchNonce } : {})
-      })}\n`,
-      { mode: 0o600 }
-    )
+    if (launchNonce === null) {
+      writeFileSync(
+        pidPath,
+        `${JSON.stringify({
+          pid: daemonIdentity.pid,
+          startedAtMs: daemonIdentity.startedAtMs,
+          entryPath,
+          fixtureNonce: randomUUID()
+        })}\n`,
+        { mode: 0o600 }
+      )
+    }
src/main/daemon/retirement-pending-claim-reconciliation.ts-21-27 (1)

21-27: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

List each daemon generation once, not once per claim.

Claims sharing a protocolVersion request the identical provider inventory. With the 4,096-item bound, this can serialize thousands of redundant daemon calls during startup maintenance. Group claims by protocol, list once per provenance, then reconcile all matching session IDs from that snapshot.

src/main/daemon/production-launcher.ts-41-48 (1)

41-48: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail the launch when v23 process identity cannot be published.

Missing/invalid startedAtMs produces a null PID record, while missing child.pid silently skips it. The v23 cleanup path then fails closed, leaving an unreplaceable daemon. A PID-record write failure also rejects without terminating the already-ready detached child.

Proposed fix
 const startedAtMs = await waitForReady(child)
-if (pidPath && launchNonce && child.pid) {
-  writeFileSync(
-    pidPath,
-    serializeDaemonPidFile({ pid: child.pid, startedAtMs, entryPath, launchNonce }),
-    { mode: 0o600 }
-  )
+if (pidPath && launchNonce) {
+  if (!child.pid || startedAtMs === null) {
+    await shutdownChild(child)
+    throw new Error('Daemon ready message did not include a complete process identity')
+  }
+  try {
+    writeFileSync(
+      pidPath,
+      serializeDaemonPidFile({ pid: child.pid, startedAtMs, entryPath, launchNonce }),
+      { mode: 0o600 }
+    )
+  } catch (error) {
+    await shutdownChild(child)
+    throw error
+  }
 }

Also applies to: 60-60, 92-95

src/main/daemon/daemon-spawner.ts-174-181 (1)

174-181: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Only discard the claim for a confirmed EEXIST replacement.

Any failed copy followed by canonicalExists() === true is treated as success. If the failed operation created a partial destination before throwing, the caller deletes the sole intact claim and preserves corrupted canonical data. Retain the claim for ENOSPC/EIO and other failures.

Proposed fix
-  } catch {
+  } catch (error) {
     // Why: only a confirmed replacement makes the unique claim redundant; an
     // I/O failure with no canonical file must retain the sole recoverable record.
-    return canonicalExists(canonicalPath)
+    return (
+      (error as NodeJS.ErrnoException).code === 'EEXIST' &&
+      canonicalExists(canonicalPath)
+    )
   }

Add a test where copyExclusive creates the canonical path and then throws ENOSPC; the result must remain false.

src/main/daemon/daemon-ownership-raw-extractor.ts-123-128 (1)

123-128: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not let provenance labels hide local bindings.

remote accepts local-shaped IDs, while local-fallback does not require the ID to be in legacyProtectedSessionIds. Either inconsistency returns a complete extraction that omits the bound local session.

Proposed fix
+  const legacyProtected = new Set(ownership.legacyProtectedSessionIds)
   for (const binding of workspace.bindings) {
     const provenance = ownership.bindingProvenanceByPtyId[binding.sessionId]
-    if (!provenance || !bindingMatchesProvenance(binding, provenance, claimByPhysicalId)) {
+    if (
+      !provenance ||
+      !bindingMatchesProvenance(binding, provenance, claimByPhysicalId, legacyProtected)
+    ) {
       return null
     }
   }

 function bindingMatchesProvenance(
   binding: RawTerminalBinding,
   provenance: TerminalBindingProvenance,
-  claimByPhysicalId: ReadonlyMap<string, DaemonSessionClaim>
+  claimByPhysicalId: ReadonlyMap<string, DaemonSessionClaim>,
+  legacyProtected: ReadonlySet<string>
 ): boolean {
   const idClass = classifyRawTerminalSessionId(binding.sessionId)
   if (provenance.kind === 'remote') {
-    return !hasClaimForSession(claimByPhysicalId, binding.sessionId)
+    return idClass === 'remote' && !hasClaimForSession(claimByPhysicalId, binding.sessionId)
   }
   if (idClass !== 'local') {
     return false
   }
   if (provenance.kind === 'local-fallback') {
-    return !hasClaimForSession(claimByPhysicalId, binding.sessionId)
+    return (
+      legacyProtected.has(binding.sessionId) &&
+      !hasClaimForSession(claimByPhysicalId, binding.sessionId)
+    )
   }

Also applies to: 159-175

src/main/daemon/daemon-ownership-raw-field-classification.ts-1-7 (1)

1-7: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Allow the known remoteSessionIdsByTabId field.

Line 18 classifies remoteSessionIdsByTabId as ownership-like, but it is absent from this allowlist. Consequently, parseRawLocalWorkspace rejects every workspace containing metadata that it explicitly validates later as unsupported-field.

Proposed fix
 const KNOWN_WORKSPACE_ID_FIELDS = new Set([
   'activeConnectionIdsAtShutdown',
   'defaultTerminalTabsAppliedByWorktreeId',
+  'remoteSessionIdsByTabId',
   'sleepingAgentSessionsByPaneKey',
   'terminalLayoutsByTabId'
 ])
src/main/daemon/daemon-reap-candidate-journal.ts-197-205 (1)

197-205: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Invalidate stale evidence when ordinary journal replacement fails.

If replaceAndReadBack fails after entries were removed or reset, the old mature journal remains readable next launch. Apply the same invalidation safeguard already used by commitResetMarker.

Proposed fix
     if (!(await this.persistence.replaceAndReadBack(encoded, this.maxBytes))) {
+      await this.persistence.invalidate(this.maxBytes)
       return this.result('incomplete', 'persistence-error', true, 0, [])
     }
src/main/daemon/daemon-ownership-raw-remote-and-legacy.ts-106-114 (1)

106-114: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject migration rows whose source contradicts the PTY ID.

An entry with source: 'ssh' currently accepts a local-shaped ID, but Line 45 ignores that row rather than protecting it. Corrupt state can therefore be reported complete while omitting a local session from ownership.

Proposed fix
 function isValidMigrationRow(value: unknown): value is { ptyId: string; source: 'local' | 'ssh' } {
-  return (
-    isRecord(value) &&
-    isId(value.ptyId) &&
-    (value.source === 'local' || value.source === 'ssh') &&
-    value.reason === 'legacy-numeric-pane-key' &&
-    Number.isFinite(value.updatedAt) &&
-    (value.source === 'ssh' || classifyRawTerminalSessionId(value.ptyId) === 'local')
-  )
+  if (
+    !isRecord(value) ||
+    !isId(value.ptyId) ||
+    (value.source !== 'local' && value.source !== 'ssh')
+  ) {
+    return false
+  }
+  const classification = classifyRawTerminalSessionId(value.ptyId)
+  return (
+    value.reason === 'legacy-numeric-pane-key' &&
+    Number.isFinite(value.updatedAt) &&
+    ((value.source === 'local' && classification === 'local') ||
+      (value.source === 'ssh' && classification === 'remote'))
+  )
 }
src/main/daemon/daemon-session-process-incarnation-windows.ts-114-123 (1)

114-123: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Report missing Windows rows as not-observed.

A successful CIM query that omits a requested PID is authoritative absence, but Line 119 conflates it with a present row lacking CreationDate. This prevents Windows probes from ever reporting dead/missing processes.

Proposed fix
     if (entries.length > 1) {
       return { pid, state: 'ambiguous' }
     }
-    if (entries.length === 0 || entries[0].creationDate === null) {
+    if (entries.length === 0) {
+      return { pid, state: 'not-observed' }
+    }
+    if (entries[0].creationDate === null) {
       return { pid, state: 'unknown' }
     }
src/main/daemon/daemon-ownership-raw-snapshot.ts-220-240 (1)

220-240: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Use a verified committed backup when the primary is unavailable.

Although Lines 136–145 capture five backups, this branch always reports state-backup-unverifiable and never calls parseValidDaemonOwnershipCommit on them. A missing/corrupt primary therefore leaves ownership permanently incomplete even when an authoritative committed backup exists, contrary to the recovery contract in the plan.

src/main/daemon/daemon-ownership-raw-snapshot.ts-125-133 (1)

125-133: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Keep large snapshot parsing and hashing off the main thread.

At the allowed 64 MiB limit, JSON.stringify, SHA-256 hashing, JSON.parse, and extraction all run synchronously. This can stall terminal I/O well beyond the documented 50 ms event-loop ceiling. Stream/incrementally hash sources and move parsing/extraction to a worker or bounded incremental pipeline.

Also applies to: 228-266

src/main/daemon/daemon-session-claims.ts-139-145 (1)

139-145: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Build the provenance map with a null prototype.

A persisted PTY ID of __proto__ changes this object’s prototype instead of creating an own entry. Downstream property lookup can then accept inherited provenance and incorrectly treat malformed ownership as complete.

Proposed fix
-  const bindingProvenanceByPtyId: Record<string, TerminalBindingProvenance> = {}
+  const bindingProvenanceByPtyId: Record<string, TerminalBindingProvenance> =
+    Object.create(null)
src/main/persistence.ts-6169-6175 (1)

6169-6175: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Scope ownership mutations to (sessionId, protocolVersion).

Line 6172 removes every claim sharing the PTY ID, Lines 6420-6443 transition all matching protocols, and Line 6481 deletes whichever provenance currently occupies the ID. Since same session IDs can coexist across protocols, a v23 bind or stop can erase or mutate v22 ownership.

Proposed direction
-            existing.sessionId === args.ptyId ||
+            (existing.sessionId === args.ptyId &&
+              existing.protocolVersion === protocolVersion) ||
             (existing.ownerKind === 'pane' &&
               existing.workspaceKey === args.worktreeId &&
               existing.ownerId === args.leafId)

Resolve the stored local-daemon provenance in transitionExistingDaemonSessionClaim and only transition claims with its protocolVersion. Likewise, delete bindingProvenanceByPtyId[sessionId] only when the stored provenance exactly matches the verified-stop provenance.

Also applies to: 6411-6443, 6475-6485

src/main/orca-profiles/profile-project-transfer-recovery.ts-112-158 (1)

112-158: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Revalidate the source before clearing the target receipt.

Lines 138-143 use captured source absence as the commit proof, but Line 155 only checks the target. If the source is active or rewritten after scanning, recovery can clear the receipt after the repo has been reintroduced, losing the recovery fence and leaving duplicate ownership. Skip active-source receipts and require every source capture to remain current.

Proposed fix
           if (
             lineage.role !== 'target-lineage' ||
             lineage.targetProfileId !== targetProfile.id ||
+            lineage.sourceProfileId === activeProfileId ||
             !knownProfileIds.has(lineage.sourceProfileId)
           ) {
             return false
           }
...
-      if (!captureStillCurrent(rawProfiles.get(targetProfile.id), targetProfile.id, userDataPath)) {
+      const sourcesStillCurrent = recoverable.every((lineage) =>
+        captureStillCurrent(
+          rawProfiles.get(lineage.sourceProfileId),
+          lineage.sourceProfileId,
+          userDataPath
+        )
+      )
+      if (
+        !sourcesStillCurrent ||
+        !captureStillCurrent(rawProfiles.get(targetProfile.id), targetProfile.id, userDataPath)
+      ) {
         continue
       }
src/main/daemon/daemon-ownership-commit-migration.ts-57-66 (1)

57-66: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Fence the migration against concurrent profile writes.

The file can change after readBoundedMigrationSource returns. A later renameSync then replaces the newer profile with the stale captured snapshot, losing unrelated state. Perform migration under the same exclusive writer lock/fence used by profile persistence.

src/main/daemon/daemon-ownership-commit-migration.ts-40-48 (1)

40-48: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Validate ownership schema independently of commit integrity.

A checksum proves consistency, not that the persisted ownership authority has a valid schema. The current write/read boundary can promote malformed ownership into trusted transfer state.

  • src/main/daemon/daemon-ownership-commit-migration.ts#L40-L48: validate the raw state and any present ownership field before backfilling a commit.
  • src/main/orca-profiles/profile-project-state-file.ts#L62-L80: reject malformed present daemonSessionOwnership before reconstructing TransferProfileState.
src/main/orca-profiles/profile-project-terminal-binding-ids.ts-34-48 (1)

34-48: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve binding IDs shared with retained projects.

all.delete(id) removes an ID whenever the moved project references it, even if another repository also references the same PTY. Source cleanup can consequently discard provenance or legacy protection for a still-retained binding. Collect bindings directly from owner keys not belonging to repoId instead of subtracting sets.

src/main/orca-profiles/profile-project-transfer-lineage.ts-28-38 (1)

28-38: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fail closed on ambiguous source-pending receipts.

Selecting the oldest candidate can resume the wrong operation when multiple source receipts match the request. Apply the same operation/target identity ambiguity check used by findTargetTransferLineage before returning a candidate.

src/main/providers/pty-natural-exit-reconciliation.ts-200-205 (1)

200-205: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Canonicalize provenance keys.

JSON.stringify preserves property insertion order, so structurally identical provenance objects can map to different keys. That can make cancellation or explicit-stop fences miss a pending exit, causing duplicate cleanup or delivery.

Build the key from each provenance variant’s fields in a fixed order, and add a regression test using differently ordered objects.

src/main/daemon/client.ts-301-306 (1)

301-306: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Require endpoint identity for protocol v23.

An absent daemonIdentity becomes null, and two null identities compare equal, so v23 can connect without authenticating PID/start-time/nonce. Reject null identities for protocol 23+ while retaining legacy compatibility.

Proposed fix
 const identity = parseDaemonEndpointIdentity(response.daemonIdentity)
-if (response.daemonIdentity !== undefined && identity === null) {
+if (
+  identity === null &&
+  (this.protocolVersion >= 23 || response.daemonIdentity !== undefined)
+) {
   finish(new DaemonProtocolError('Invalid daemon identity'))
   return
 }

Also applies to: 440-450

src/main/daemon/daemon-init.ts-803-817 (1)

803-817: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep legacy listeners bound while launching the replacement.

unbindLocalProviderListeners() runs before ensureRunning() and legacy rediscovery. Data or exits from preserved legacy PTYs during those awaits are dropped. Build and discover the replacement first, then unbind, dispose old subscriptions, swap, and rebind synchronously.

Also applies to: 843-850, 853-868

src/main/ipc/pty.ts-607-617 (1)

607-617: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not finalize when ownership removal rejects the stop.

When mayForgetRoute is false, this still returns true. Callers then finalize shutdown and deliver an exit despite the durable binding refusing route removal, potentially clearing replacement state.

Proposed fix
   const mayForgetRoute =
     store?.removePtyBindingOwnershipAfterVerifiedStop({
       sessionId: id,
       provenance: expectedProvenance
     }) ?? true
-  if (mayForgetRoute) {
-    if (provider.forgetPtyRouteAfterVerifiedStop?.(id, expectedProvenance) === false) {
-      return false
-    }
+  if (!mayForgetRoute) {
+    return false
+  }
+  if (provider.forgetPtyRouteAfterVerifiedStop?.(id, expectedProvenance) === false) {
+    return false
   }
   return true
src/main/ipc/pty.ts-2962-2966 (1)

2962-2966: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Preserve the verified exit when cleanup throws.

The pending proof is cancelled before durable cleanup. If cleanup throws, reconciliation cannot retry and the exited binding remains stuck.

Proposed fix
         if (payload.verifiedAbsent) {
-          naturalPtyExitReconciliation.cancel(payload.id, payload.provenance)
-          if (finishVerifiedPtyBindingStop(localProvider, payload.id, store, payload.provenance)) {
+          let didFinish: boolean
+          try {
+            didFinish = finishVerifiedPtyBindingStop(
+              localProvider,
+              payload.id,
+              store,
+              payload.provenance
+            )
+          } catch {
+            naturalPtyExitReconciliation.enqueue(exit)
+            return
+          }
+          naturalPtyExitReconciliation.cancel(payload.id, payload.provenance)
+          if (didFinish) {
             deliverConfirmedNaturalExit(exit)
           }
src/main/daemon/degraded-daemon-pty-provider.ts-272-278 (1)

272-278: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Roll back the local fence when daemon fencing fails.

If current.beginRestartFence() rejects, restartFenced remains true and future spawn/attach operations stay blocked.

Proposed fix
   async beginRestartFence(): Promise<string[]> {
     this.restartFenced = true
-    if (this.createOrAttachInFlight > 0) {
-      await new Promise<void>((resolve) => this.createOrAttachDrainWaiters.add(resolve))
+    try {
+      if (this.createOrAttachInFlight > 0) {
+        await new Promise<void>((resolve) => this.createOrAttachDrainWaiters.add(resolve))
+      }
+      return await this.current.beginRestartFence()
+    } catch (error) {
+      this.restartFenced = false
+      this.current.cancelRestartFence()
+      throw error
     }
-    return this.current.beginRestartFence()
   }
🟡 Minor comments (3)
tests/e2e/helpers/daemon-generation-fixtures.ts-265-272 (1)

265-272: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a reported start identity when validation is enabled.

A missing or non-numeric startedAtMs silently bypasses validation. Only the official legacy fixture disables this contract check.

Proposed fix
     const reportedStart = ready.startedAtMs
-    if (
-      validateReportedStart &&
-      typeof reportedStart === 'number' &&
-      Math.abs(reportedStart - daemonIdentity.startedAtMs) > 2_000
-    ) {
-      throw new Error(`${label} daemon self-reported a different process incarnation`)
+    if (validateReportedStart) {
+      if (
+        typeof reportedStart !== 'number' ||
+        !Number.isFinite(reportedStart) ||
+        Math.abs(reportedStart - daemonIdentity.startedAtMs) > 2_000
+      ) {
+        throw new Error(`${label} daemon self-reported a different process incarnation`)
+      }
     }
tests/e2e/daemon-generation-reaping.spec.ts-200-217 (1)

200-217: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Close the child-exit listener race.

The child can exit after Lines 204-206 but before the listener is registered. Since exit is not replayed, this then waits until the 10-second timeout.

Proposed fix
   return await new Promise((resolve, reject) => {
     const timer = setTimeout(
       () => reject(new Error('Timed out waiting for fixture daemon exit')),
       timeoutMs
     )
-    child.once('exit', (code, signal) => {
+    const onExit = (code: number | null, signal: NodeJS.Signals | null): void => {
       clearTimeout(timer)
       resolve({ code, signal })
-    })
+    }
+    child.once('exit', onExit)
+    if (child.exitCode !== null || child.signalCode !== null) {
+      child.off('exit', onExit)
+      clearTimeout(timer)
+      resolve({ code: child.exitCode, signal: child.signalCode })
+    }
   })
src/main/daemon/degraded-daemon-provider-events.ts-27-40 (1)

27-40: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Snapshot listener arrays before fanout.

A listener that unsubscribes itself mutates the live array and can skip the next data or exit listener. Mirror the synthetic-exit implementation.

Proposed fix
-          for (const listener of this.dataListeners) {
+          for (const listener of this.dataListeners.slice()) {
             listener(payload)
           }
...
-          for (const listener of this.exitListeners) {
+          for (const listener of this.exitListeners.slice()) {
             listener({
🧹 Nitpick comments (4)
tests/e2e/helpers/daemon-generation-processes.ts (1)

216-224: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Enumerate the process table once per aggregate check.

This currently runs a full ps/CIM query for every identity. A single polling predicate can therefore take identityCount × 3 seconds, defeating the surrounding three- and five-second deadlines.

Proposed refactor
 export async function anyRecordedProcessIsAlive(
   identities: RecordedProcessIdentity[]
 ): Promise<boolean> {
-  for (const identity of identities) {
-    if (await processIdentityIsAlive(identity)) {
-      return true
-    }
-  }
-  return false
+  const rows = await readProcessRows()
+  return identities.some((identity) => {
+    const current = rows.find((row) => row.pid === identity.pid)
+    return current?.startedAtMs === identity.startedAtMs
+  })
 }

Source: Coding guidelines

src/main/daemon/retirement-pending-claim-reconciliation.test.ts (1)

52-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the verified stale route is forgotten.

This case should verify forgetPtyRouteAfterVerifiedStop receives pending-session and the protocol-22 provenance; otherwise the route-cleanup half of successful reconciliation can regress unnoticed.

Proposed assertion
 expect(store.clearVerifiedRetirementPendingDaemonClaim).toHaveBeenCalledOnce()
+expect(provider.forgetPtyRouteAfterVerifiedStop).toHaveBeenCalledWith('pending-session', {
+  kind: 'local-daemon',
+  protocolVersion: 22
+})
src/main/daemon/daemon-session-process-incarnation-posix.ts (1)

7-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Construct /proc paths with Node path utilities.

Use path.posix.join for both the boot-ID and PID-stat paths rather than embedding separators.

As per coding guidelines, “Use path.join or Electron/Node path utilities for file paths; never assume / or \.”

Also applies to: 45-45

Source: Coding guidelines

src/main/daemon/daemon-server.ts (1)

783-802: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Scope the shutdown switch clause.

const serverClose currently occupies the switch-wide lexical scope and triggers Biome’s noSwitchDeclarations error. Wrap this case in braces.

Proposed fix
-      case 'shutdown':
+      case 'shutdown': {
         this.log.log('shutdown', {
           reason: 'rpc',
           killSessions: request.payload.killSessions === true
         })
         const serverClose = this.beginOrdinaryShutdownFence()
         // ...
         return {}
+      }

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c86cd00d-b379-4085-b8ac-765e3d95b3a0

📥 Commits

Reviewing files that changed from the base of the PR and between 0532765 and 7c91590.

📒 Files selected for processing (106)
  • config/reliability-gates.jsonc
  • docs/reference/plans/2026-07-17-stale-terminal-daemon-generation-reaping.md
  • src/main/daemon/client.test.ts
  • src/main/daemon/client.ts
  • src/main/daemon/daemon-entry.test.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-foreground-confirmation-protocol.test.ts
  • src/main/daemon/daemon-generation-audit.test.ts
  • src/main/daemon/daemon-generation-audit.ts
  • src/main/daemon/daemon-generation-inventory.ts
  • src/main/daemon/daemon-health.test.ts
  • src/main/daemon/daemon-health.ts
  • src/main/daemon/daemon-hello-protocol.ts
  • src/main/daemon/daemon-idle-shutdown.test.ts
  • src/main/daemon/daemon-init.test.ts
  • src/main/daemon/daemon-init.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/daemon-ownership-commit-migration.test.ts
  • src/main/daemon/daemon-ownership-commit-migration.ts
  • src/main/daemon/daemon-ownership-commit.ts
  • src/main/daemon/daemon-ownership-profile-index.ts
  • src/main/daemon/daemon-ownership-raw-extractor.test.ts
  • src/main/daemon/daemon-ownership-raw-extractor.ts
  • src/main/daemon/daemon-ownership-raw-field-classification.ts
  • src/main/daemon/daemon-ownership-raw-filesystem.ts
  • src/main/daemon/daemon-ownership-raw-remote-and-legacy.ts
  • src/main/daemon/daemon-ownership-raw-snapshot.test.ts
  • src/main/daemon/daemon-ownership-raw-snapshot.ts
  • src/main/daemon/daemon-ownership-raw-workspace.ts
  • src/main/daemon/daemon-pty-adapter.test.ts
  • src/main/daemon/daemon-pty-adapter.ts
  • src/main/daemon/daemon-pty-router.test.ts
  • src/main/daemon/daemon-pty-router.ts
  • src/main/daemon/daemon-reap-candidate-journal-file.ts
  • src/main/daemon/daemon-reap-candidate-journal-schema.ts
  • src/main/daemon/daemon-reap-candidate-journal-storage.test.ts
  • src/main/daemon/daemon-reap-candidate-journal.test.ts
  • src/main/daemon/daemon-reap-candidate-journal.ts
  • src/main/daemon/daemon-reconciliation-coordinator.test.ts
  • src/main/daemon/daemon-reconciliation-coordinator.ts
  • src/main/daemon/daemon-server.test.ts
  • src/main/daemon/daemon-server.ts
  • src/main/daemon/daemon-session-claims.ts
  • src/main/daemon/daemon-session-process-incarnation-posix.ts
  • src/main/daemon/daemon-session-process-incarnation-windows.ts
  • src/main/daemon/daemon-session-process-incarnation.test.ts
  • src/main/daemon/daemon-session-process-incarnation.ts
  • src/main/daemon/daemon-spawner.test.ts
  • src/main/daemon/daemon-spawner.ts
  • src/main/daemon/degraded-daemon-fallback-shutdown.ts
  • src/main/daemon/degraded-daemon-provider-events.ts
  • src/main/daemon/degraded-daemon-pty-provider.test.ts
  • src/main/daemon/degraded-daemon-pty-provider.ts
  • src/main/daemon/degraded-daemon-session-routes.ts
  • src/main/daemon/production-launcher.test.ts
  • src/main/daemon/production-launcher.ts
  • src/main/daemon/retirement-pending-claim-reconciliation.test.ts
  • src/main/daemon/retirement-pending-claim-reconciliation.ts
  • src/main/daemon/types.ts
  • src/main/index.ts
  • src/main/ipc/orca-profile-project-transfer-handler.ts
  • src/main/ipc/orca-profiles.test.ts
  • src/main/ipc/orca-profiles.ts
  • src/main/ipc/pty.test.ts
  • src/main/ipc/pty.ts
  • src/main/orca-profiles/profile-cloud-index.ts
  • src/main/orca-profiles/profile-cloud-service.test.ts
  • src/main/orca-profiles/profile-index-initialization.ts
  • src/main/orca-profiles/profile-index-store.test.ts
  • src/main/orca-profiles/profile-index-store.ts
  • src/main/orca-profiles/profile-ownership-authority-seed.ts
  • src/main/orca-profiles/profile-project-daemon-ownership-transfer.test.ts
  • src/main/orca-profiles/profile-project-daemon-ownership-transfer.ts
  • src/main/orca-profiles/profile-project-session-state.ts
  • src/main/orca-profiles/profile-project-session-transfer.ts
  • src/main/orca-profiles/profile-project-source-removal.ts
  • src/main/orca-profiles/profile-project-state-file.ts
  • src/main/orca-profiles/profile-project-terminal-binding-ids.ts
  • src/main/orca-profiles/profile-project-transfer-lineage-raw.test.ts
  • src/main/orca-profiles/profile-project-transfer-lineage.ts
  • src/main/orca-profiles/profile-project-transfer-payload.ts
  • src/main/orca-profiles/profile-project-transfer-recovery.test.ts
  • src/main/orca-profiles/profile-project-transfer-recovery.ts
  • src/main/orca-profiles/profile-project-transfer-topology.ts
  • src/main/orca-profiles/profile-project-transfer.test.ts
  • src/main/orca-profiles/profile-project-transfer.ts
  • src/main/persistence.test.ts
  • src/main/persistence.ts
  • src/main/providers/local-pty-provider.ts
  • src/main/providers/pty-binding-provider.ts
  • src/main/providers/pty-exit-payload.ts
  • src/main/providers/pty-natural-exit-reconciliation.test.ts
  • src/main/providers/pty-natural-exit-reconciliation.ts
  • src/main/providers/types.ts
  • src/main/runtime/orca-runtime.test.ts
  • src/main/runtime/orca-runtime.ts
  • src/shared/constants.ts
  • src/shared/daemon-session-ownership.ts
  • src/shared/orca-profiles.ts
  • src/shared/types.ts
  • tests/e2e/daemon-generation-reaping.spec.ts
  • tests/e2e/fixtures/daemon-generation-entry.ts
  • tests/e2e/fixtures/daemon-generation-marker.cjs
  • tests/e2e/helpers/daemon-generation-fixtures.ts
  • tests/e2e/helpers/daemon-generation-processes.ts
  • tests/e2e/helpers/daemon-generation-v21-release.ts

@Jinwoo-H
Jinwoo-H force-pushed the Jinwoo-H/investigate-issue-9138 branch from 7c91590 to 4fff463 Compare July 18, 2026 21:08
@Jinwoo-H Jinwoo-H changed the title fix(daemon): prevent new idle generation buildup and audit legacy reaping fix(daemon): retire empty current-generation daemons Jul 18, 2026

@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: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 75a984d9-e4a0-4e8a-ae87-be379c63dbba

📥 Commits

Reviewing files that changed from the base of the PR and between 7c91590 and 4fff463.

📒 Files selected for processing (22)
  • docs/reference/plans/2026-07-18-daemon-lifecycle-retirement.md
  • src/main/daemon/client.test.ts
  • src/main/daemon/client.ts
  • src/main/daemon/daemon-entry.test.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-foreground-confirmation-protocol.test.ts
  • src/main/daemon/daemon-hello-protocol.ts
  • src/main/daemon/daemon-idle-shutdown.test.ts
  • src/main/daemon/daemon-init.test.ts
  • src/main/daemon/daemon-init.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/daemon-pty-adapter.ts
  • src/main/daemon/daemon-self-retirement-respawn.test.ts
  • src/main/daemon/daemon-server.test.ts
  • src/main/daemon/daemon-server.ts
  • src/main/daemon/daemon-spawner.test.ts
  • src/main/daemon/daemon-spawner.ts
  • src/main/daemon/production-launcher.test.ts
  • src/main/daemon/production-launcher.ts
  • src/main/daemon/types.ts
  • tests/e2e/daemon-lifecycle-retirement.spec.ts
  • tests/e2e/fixtures/daemon-lifecycle-entry.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/main/daemon/production-launcher.test.ts
  • src/main/daemon/daemon-spawner.test.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/daemon-server.test.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-spawner.ts

Comment thread src/main/daemon/daemon-entry.test.ts
Comment thread src/main/daemon/daemon-init.test.ts Outdated
Comment thread src/main/daemon/daemon-server.ts Outdated
Comment thread tests/e2e/daemon-lifecycle-retirement.spec.ts Outdated
@Jinwoo-H
Jinwoo-H force-pushed the Jinwoo-H/investigate-issue-9138 branch from 4fff463 to 1569c55 Compare July 19, 2026 23:20

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

🧹 Nitpick comments (2)
src/main/daemon/daemon-init.ts (1)

273-339: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider sharing the child-shutdown helper with src/main/daemon/production-launcher.ts.
terminateLaunchedDaemonChild and shutdownChild duplicate the SIGTERM/SIGKILL cleanup path and ESRCH handling, so the two launchers can drift over time.

src/main/daemon/production-launcher.ts (1)

16-67: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Partial pidPath/launchNonce silently skips identity publication.

The fork-args gate (pidPath && launchNonce, Line 31) and the PID-write gate (Line 49) both require both values. If a caller ever passes only one (e.g. a future bug in the daemon-spawner.ts call site), the launcher silently skips ownership-record creation and CLI flags with no error — unlike daemon-entry.ts's parseArgs, which explicitly throws when only one of --pid-record/--launch-nonce is supplied. Consider asserting this invariant on the launcher side too, so a caller mistake fails loudly instead of producing a daemon with no ownership metadata.

🛡️ Suggested guard
     const entryPath = opts.getDaemonEntryPath()
+
+    if ((pidPath && !launchNonce) || (!pidPath && launchNonce)) {
+      throw new Error('pidPath and launchNonce must be provided together')
+    }

Please confirm the daemon-spawner.ts call site always passes both together (or neither) for this launcher.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a57b3bc7-f40a-4ffa-8bb0-6d26a812dbec

📥 Commits

Reviewing files that changed from the base of the PR and between 4fff463 and 1569c55.

📒 Files selected for processing (21)
  • docs/reference/plans/2026-07-18-daemon-lifecycle-retirement.md
  • src/main/daemon/client.test.ts
  • src/main/daemon/client.ts
  • src/main/daemon/daemon-entry.test.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-foreground-confirmation-protocol.test.ts
  • src/main/daemon/daemon-hello-protocol.ts
  • src/main/daemon/daemon-idle-shutdown.test.ts
  • src/main/daemon/daemon-init.test.ts
  • src/main/daemon/daemon-init.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/daemon-pty-adapter.ts
  • src/main/daemon/daemon-self-retirement-respawn.test.ts
  • src/main/daemon/daemon-server.test.ts
  • src/main/daemon/daemon-server.ts
  • src/main/daemon/daemon-spawner.test.ts
  • src/main/daemon/daemon-spawner.ts
  • src/main/daemon/production-launcher.test.ts
  • src/main/daemon/production-launcher.ts
  • src/main/daemon/types.ts
  • tests/e2e/daemon-lifecycle-retirement.spec.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • src/main/daemon/daemon-foreground-confirmation-protocol.test.ts
  • src/main/daemon/daemon-entry.test.ts
  • src/main/daemon/daemon-hello-protocol.ts
  • src/main/daemon/daemon-spawner.test.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/types.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-server.test.ts
  • src/main/daemon/daemon-spawner.ts
  • src/main/daemon/client.test.ts
  • src/main/daemon/client.ts
  • src/main/daemon/daemon-init.test.ts

@Jinwoo-H
Jinwoo-H force-pushed the Jinwoo-H/investigate-issue-9138 branch from 1569c55 to c8a98b7 Compare July 20, 2026 00:29
Jinwoo-H and others added 4 commits July 19, 2026 17:53
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Orca <help@stably.ai>
@Jinwoo-H
Jinwoo-H force-pushed the Jinwoo-H/investigate-issue-9138 branch from c8a98b7 to cdc89e1 Compare July 20, 2026 00:55

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

🧹 Nitpick comments (1)
src/main/daemon/production-launcher.ts (1)

138-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared child-termination protocol into one helper. Both launchers implement the same SIGTERM→5s grace→SIGKILL→1s force-exit sequence (with identical timings) plus a copy of isNoSuchProcessError. Keeping two copies of this safety-critical teardown in sync is error-prone; a fix to one (timing, exit-listener handling, ESRCH tolerance) could silently miss the other.

  • src/main/daemon/production-launcher.ts#L138-L221: replace shutdownChild/isNoSuchProcessError with the shared helper (its 5s/1s intervals are hardcoded here vs. named constants in daemon-init).
  • src/main/daemon/daemon-init.ts#L273-L339: replace terminateLaunchedDaemonChild/isNoSuchProcessError with the same shared helper, passing the grace/force constants.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bc2113a2-9336-46c7-845a-580109955431

📥 Commits

Reviewing files that changed from the base of the PR and between 1569c55 and cdc89e1.

📒 Files selected for processing (23)
  • config/scripts/windows-daemon-workspace-close-repro.mjs
  • docs/reference/plans/2026-07-18-daemon-lifecycle-retirement.md
  • src/main/daemon/client.test.ts
  • src/main/daemon/client.ts
  • src/main/daemon/daemon-entry.test.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-foreground-confirmation-protocol.test.ts
  • src/main/daemon/daemon-hello-protocol.ts
  • src/main/daemon/daemon-idle-shutdown.test.ts
  • src/main/daemon/daemon-init.test.ts
  • src/main/daemon/daemon-init.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/daemon-pty-adapter.ts
  • src/main/daemon/daemon-self-retirement-respawn.test.ts
  • src/main/daemon/daemon-server.test.ts
  • src/main/daemon/daemon-server.ts
  • src/main/daemon/daemon-spawner.test.ts
  • src/main/daemon/daemon-spawner.ts
  • src/main/daemon/production-launcher.test.ts
  • src/main/daemon/production-launcher.ts
  • src/main/daemon/types.ts
  • tests/e2e/daemon-lifecycle-retirement.spec.ts
  • tests/e2e/fixtures/daemon-lifecycle-entry.ts
🚧 Files skipped from review as they are similar to previous changes (15)
  • src/main/daemon/daemon-foreground-confirmation-protocol.test.ts
  • src/main/daemon/daemon-entry.test.ts
  • src/main/daemon/daemon-hello-protocol.ts
  • tests/e2e/fixtures/daemon-lifecycle-entry.ts
  • src/main/daemon/types.ts
  • src/main/daemon/client.test.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-spawner.test.ts
  • src/main/daemon/daemon-self-retirement-respawn.test.ts
  • src/main/daemon/production-launcher.test.ts
  • src/main/daemon/daemon-server.test.ts
  • src/main/daemon/daemon-spawner.ts
  • src/main/daemon/client.ts
  • src/main/daemon/daemon-idle-shutdown.test.ts
  • src/main/daemon/daemon-pty-adapter.ts

@OrcaWin
OrcaWin merged commit 7adda25 into main Jul 20, 2026
8 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.

2 participants