Skip to content

fix(orcad): publish the headless graph and start the agent hook server - #17735

Closed
klaidliadon wants to merge 1 commit into
stablyai:mainfrom
klaidliadon:fix/orcad-headless-graph-and-agent-hooks
Closed

klaidliadon wants to merge 1 commit into
stablyai:mainfrom
klaidliadon:fix/orcad-headless-graph-and-agent-hooks

Conversation

@klaidliadon

@klaidliadon klaidliadon commented Aug 31, 2026 •

Copy link
Copy Markdown

ELI5

orcad skips three startup steps that orca serve performs. Because of that it
cannot create a terminal at all, and its agent chat view renders nothing. Each
fix is one small piece of wiring that --serve already had.

What Changed

Five commits in src/main/orcad/orcad-entry.ts, plus a test that pins the wiring.

1. Publish the headless graph

orcad never called runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, …).
--serve does, with a comment naming this exact hazard — "headless servers have
no renderer graph publisher; publish an explicit empty graph so status clients see
a ready server."
orcad copies the surrounding sequence
(rehydrateClientHostedBrowserPages, refreshRestoredOrchestrationAuthority,
reconcileLegacyWorkerTerminals) and omits that one line, so graphStatus never
leaves 'unavailable'.

Two failures, one cause, and neither names the graph:

  • session.tabs.createTerminal returns runtime_unavailable. The throw is
    assertGraphReady() via captureReadyGraphEpoch(), the first statement of
    runCreateMobileSessionTerminal — above the authoritative-window check and far
    above any PTY code. A live daemon and an installed ptyController.spawn make no
    difference because neither is reached.
  • session.tabs.listAll hangs. getAuthoritativeSessionTabsInventoryEpoch()
    returns non-null only when graphStatus === 'ready', so the while (true) in
    listAllMobileSessionTabsInventory waits on a publication that
    markSessionTabsInventoryPublished early-returns out of once the epochs match.
    No timeout, no error, no log — an RPC that never answers.

One call opens both gates: it adopts HEADLESS_RUNTIME_WINDOW_ID as
authoritative, marks the graph ready, and publishes the tab inventory.

2. Start the agent hook server

Native chat reads the agent's transcript file, and session-file-resolver.ts names
the source: "Authoritative transcript path reported by the agent hook
(providerSession.transcriptPath)"
.

The spawn path was already correct — host-env/assembly.ts folds
agentHookServer.buildPtyEnv() into every PTY's environment — but buildPtyEnv()
opens with if (this.port <= 0 || !this.token) return {}, and with no server the
port is 0. Measured on the live agent process before the change: eight ORCA_*
vars present, and zero ORCA_AGENT_HOOK_*. So claude-hook.sh returned at its
first line on every invocation.

userDataPath is load-bearing, not incidental: ORCA_AGENT_HOOK_ENDPOINT is set
only when the endpoint file was written, and it is written only when start()
received that path. It matters more here than on the desktop, because start()
regenerates token and port every launch while orcad's terminal daemon deliberately
outlives the runtime — so a surviving PTY carries stale coordinates that the hook
repairs only by sourcing the endpoint file at invocation time.

3. Wire the hook read seams

Starting the server only opened the write path. The runtime was constructed
with none of the agent-hook getters the desktop host passes, so every row the
server collected reached nothing: getHookRowsForPane answered [] for each pane,
published tabs carried no providerSession.transcriptPath, and
hasProviderSessionObservationSource() answered false — which makes structured
Claude resume refuse for want of a fresh provider session.

The filter asymmetry is deliberate, not cosmetic: getAgentStatusSnapshot hides
resume-identity rows from live-agent views while getAgentProviderSessionSnapshot
keeps them, because those rows carry the provider session native chat addresses
transcripts by. Taking one without the other fixes chat and breaks the live-agent
rows.

buildAgentHookPtyEnv stays unwired on purpose — host-env/assembly.ts reads the
same singleton directly, so passing it here would add a second source for env this
host already gets.

Also stops the server when a launch fails: startOrcad's failure path undoes the
browser provider and the instance lock but never imported the hook server, so a
step throwing after start() left the listener bound — and start() returns early
while this.server is set, handing the next attempt in the same process a stale
token and endpoint file.

4. Pin the wiring in a test

So a future refactor cannot drop these steps silently, which is how all three went
missing in the first place.

5. Finish teardown when a step rejects

Added in review. The shutdown closure ran its teardown steps as a flat sequence, so
the first rejection skipped every later one — and both awaits in it genuinely
reject. stopOrcadDaemon() reaches disconnectOnly(), which fans out with
Promise.all over every adapter and awaits an unguarded checkpoint write;
browserProvider.stop() ends in rm(..., { force: true }), and force forgives
only ENOENT.

The step furthest down that sequence is instanceLock.release(). Leaking the lock
file makes the next orcad launch refuse to start until someone deletes it by
hand, so a transient teardown fault became a dead host. agentHookServer.stop()
was dropped the same way, which is what review caught; the leaked lock is the same
bug with a worse blast radius, so both are fixed at the source.

startOrcad's launch-failure path had a third form: a rejecting
browserProvider.stop() there replaced the error explaining why startup failed and
skipped the lock release, so a launch that failed once could not be retried. It now
catches and logs that, and rethrows the original.

⚠️ Commit 3 is necessary but not sufficient — please read before merging

This is stated deliberately rather than left for the next person to rediscover.

With this deployed on a Synology NAS, the hook server holds rows for both live
panes with transcriptPath populated — one received 22:25:03, one 22:35:53, both
after the restart — and session.tabs.listAll still reports no
providerSession on any tab.
So the input is wired and something downstream of
it still declines.

The prime suspect is the merge's own recency gate:

(hookAgentStatus.providerSessionReceivedAt ?? -1) >= tab.agentStatus.updatedAt

While the accessors were missing, this was unreachable — the && short-circuited
on an undefined hookAgentStatus. Wiring them makes it live for the first
time
, and agent status updates arrive from terminal parsing independently of hook
delivery, so a status update landing after the hook inverts that comparison
permanently.

That is a hypothesis from reading the condition, not a measured result, and it
wants an instrumented build to settle. I'd rather land the wiring — which is
correct and required either way — and flag this honestly than imply chat is fixed.

There is also a diagnosis worth recording: this third one is not a missing startup
step but a missing constructor dependency, so a shared startup sequence would
not have caught it. A shared factory that builds the runtime with its full deps
would.

Known limitations

A PTY retained across the upgrade that introduces this commit stays hookless.
ORCA_AGENT_HOOK_* reaches a terminal only through its spawn-time environment, and
a running process's environment is frozen. So a PTY the daemon carries across this
upgrade has no vars at all — claude-hook.sh returns at its first line, with no
endpoint path to source. It self-heals when that terminal is recreated.

This is distinct from the stale coordinates case the endpoint file does solve: a
PTY spawned by a previous orcad that already had the hook server has wrong port and
token in its frozen env, and recovers because the hook re-reads the endpoint file at
invocation. That is why userDataPath is load-bearing here.

Closing the first case would need the hook to locate the endpoint without an env
pointer — a fixed well-known path — which is a different trust model and wants its
own design rather than a rider on this commit. Raised by Greptile in review.

Linked Issue

Fixes #17846

Visual Proof

N/A — headless runtime, no UI in this process. The client-visible effect is an
RPC that answers instead of hanging; before/after RPC output is in Testing below.

Testing

Rebased onto main and rebuilt before submitting, rather than assuming the branch
it was developed on still applied. On a scratch data root:

session.tabs.listAll         returns {"snapshots":[]}   (was: no response, 60s)
session.tabs.createTerminal  ok, tab.status "ready"
daemon log                   session-created            (was: no create request, ever)
new PTY env                  6/6 ORCA_AGENT_HOOK_*      (was: 0)
agent-hooks/                 dir 0700, endpoint.env 0600
build                        3187 modules, zero electron and node:sqlite imports

The daemon line is the one that matters for the first commit: before it, the
terminal daemon never received a create request at all, from any client, on any
attempt. It was healthy throughout and simply never asked.

Platforms: Linux (Synology DSM), as a real system-scope systemd service with a
paired iOS client. orcad is a headless Linux/Unix target; no macOS or Windows
surface.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Not run in this environment: tsc and oxlint — neither is installable on the
host this was developed and verified on. CI is the authority for those.

AI Disclosure

Claude Opus 5 wrote the commits and this description. I reviewed them and ran all
the NAS verification above.

Review

  • Cross-platform: orcad is a headless Node host; these are startup-sequence
    additions with no platform branch. File modes on the agent-hooks directory (0700
    / 0600) follow the existing serve-path behaviour.
  • SSH/remote/local: this is the remote-host case — the whole point is a runtime
    reached over an SSH port-forward by a paired client. Verified in exactly that
    configuration.
  • Agents/integrations: the hook server is provider-neutral; the resume-identity
    filter asymmetry is preserved exactly as the desktop host has it, so no provider
    is treated specially here.
  • Performance: one graph publication and one listener bind at startup. The
    accessors are pass-through reads over an in-memory snapshot.
  • UI: none in-process; unblocks the client's chat view.
  • Security: the hook server binds loopback and writes an endpoint file at 0600
    in a 0700 directory, matching --serve. endpointNamespace is deliberately not
    set — it is dev-worktree only, and namespacing would break endpoint-file
    stability across the restarts this host exists to survive. Fail-open on start,
    matching --serve.
  • Backwards compatibility: additive startup steps on a path that currently
    cannot create a terminal, so there is no working behaviour to regress.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

The caveat above is the next person's starting point; please don't let it get lost
in the merge.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@greptile-apps

greptile-apps Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Initializes the headless graph during startup.

The reviewed changes appear safe to merge; no outstanding finding is included in the merge assessment.

Summary

This PR publishes orcad’s headless graph before accepting RPC connections and adds a test for that startup ordering.

  • The graph publication follows PTY registration and recovery, matching the existing headless serve sequence.
  • No new actionable issue was established in the changes since the previous review.
Diagram
sequenceDiagram
  participant Startup as orcad startup
  participant Runtime
  participant RPC
  Startup->>Runtime: Register and hydrate PTYs
  Startup->>Runtime: Reconcile restored state
  Startup->>Runtime: Publish empty headless graph
  Runtime-->>Startup: Graph and tab inventory ready
  Startup->>RPC: Start accepting connections
Loading

Reviews (5) · Last reviewed commit: "fix(orcad): publish the headless graph s..."

Comment thread src/main/orcad/orcad-entry.ts Outdated
@coderabbitai

coderabbitai Bot commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
🧰 Additional context used
📚 Code guidelines (1)
AGENTS.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: ae4b0739-86a9-48de-94be-17a383f2b928

📥 Commits

Reviewing files that changed from the base of the PR and between 3dd8f20a117d3cf0d406bc91df731ffc475910f6 and b65bf8c.

📒 Files selected for processing (2)
  • src/main/orcad/orcad-entry.ts
  • src/main/startup/headless-pty-hydration-ordering.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The Orcad startup path now imports HEADLESS_RUNTIME_WINDOW_ID and synchronizes an empty graph after status-capture attachment and before RPC startup. A source-order test checks that headless PTY registration precedes synchronization and RPC server construction follows it.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to b65bf

The headless graph is ready before RPC requests are served, so the reported startup readiness issue is addressed. No merge-blocking risk remains in this change.

Architecture Summary

Architecture risk: 🔵 Low · up to b65bf

The change affects 1 system.

Changed systems: src

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — src (service) was modified; 2 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in src/main/orcad/orcad-entry.ts: Added the HEADLESS_RUNTIME_WINDOW_ID import for identifying the headless runtime window.
  • observed — Modified behavior in src/main/orcad/orcad-entry.ts: After attaching observed-status capture, startOrcadRuntime now synchronizes an empty graph for the headless runtime window before RPC startup.
  • observed — Modified behavior in src/main/startup/headless-pty-hydration-ordering.test.ts: Adds a source-order test asserting that headless PTY registration exists, the headless graph synchronization follows it, and RPC server construction follows the synchronization.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning [#17846] orcad now publishes the empty headless graph after PTY registration and before RPC startup. This addresses graph readiness and the graph-gated terminal and tab RPCs. The head already starts… Expose hook-reported provider-session transcript data through session.tabs.listAll and add an automated regression test for that response.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title names two major changes: publishing the headless graph and starting the agent hook server. It omits the hook accessors and teardown changes, but it still clearly describes important parts of…
Description check ✅ Passed The description is detailed and covers the template’s main sections, including the changes, rationale, linked issue, visual proof, testing, review considerations, and checklist. It also discloses unru…
Out of Scope Changes check ✅ Passed The whole-PR diff adds headless graph publication and a startup-order test. Both changes directly address [#17846]. No unrelated changes appear in the diff.
Full details: Linked Issues check

Explanation

[#17846] orcad now publishes the empty headless graph after PTY registration and before RPC startup. This addresses graph readiness and the graph-gated terminal and tab RPCs. The head already starts agentHookServer with userDataPath and supplies hook environment variables to PTYs. However, the PR summary reports that session.tabs.listAll still does not expose providerSession, which carries the transcript path required by native chat. That leaves the linked issue's agent-chat requirement unmet.

  • Fix all pre-merge checks with AI

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: de82cbfb-52e1-4425-9964-bb82486979d7

📥 Commits

Reviewing files that changed from the base of the PR and between aa658d2 and 491f2dfaf49918829af645dcf990c3812d6a858f.

📒 Files selected for processing (1)
  • src/main/orcad/orcad-entry.ts

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

Comment thread src/main/orcad/orcad-entry.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The agent-hook server starts, but the runtime is never wired to read hook status back, so hook-reported status — including the providerSession.transcriptPath native chat addresses transcripts by — still never reaches the paired client.

Reviewed changes

  • Publish the headless graph — runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] }) mirrors the --serve sequence (src/main/index.ts:3480), so captureReadyGraphEpoch() stops throwing runtime_unavailable and the tab inventory epoch is established.
  • Start the agent hook server — agentHookServer.start({ env: 'production', userDataPath }) runs before the daemon, gated on isAgentStatusHooksEnabled, fail-open, with a paired stop() in the shutdown closure.

⚠️ Hook status is written but never read back — agent chat still has no transcript

The syncWindowGraph half is complete and correct, but the hook-server half is only the write path. Starting the server means hooks can now POST and buildPtyEnv() returns real coordinates (the verified 6/6 ORCA_AGENT_HOOK_*). But OrcaRuntimeService in orcad-entry.ts is constructed with only getLocalProvider/getSshProvider/canRecoverPersistentLocalPtys/getDesktopWindowStatus. None of the agent-hook read seams are wired — no getAgentProviderSessionSnapshot, getAgentProviderSessionRowsForPane, getAgentStatusSnapshot, or onTerminalAgentStatus.

Consequently toMobileSessionTabsResult (src/main/runtime/orca-runtime.ts:36844) falls back to an empty getAgentProviderSessionSnapshotFn, so providerSession is never attached to session.tabs.listAll results, and the mobile chat still has no transcript address. The comment at orca-runtime.ts:37253 states this exact dependency: "only the hook payload does, and headless serve has no renderer to publish tab.agentStatus." The verification list in the PR body confirms the write path (env vars, endpoint file, terminal creation) but does not actually verify that a transcript reaches the chat view.

Technical details
# Wire the agent-hook read path into orcad's runtime

## Affected sites
- `src/main/orcad/orcad-entry.ts:203` — `new OrcaRuntimeService(store, undefined, { ... })` omits the agent-hook deps.
- `src/main/index.ts:2884-2929` — the `--serve` construction wires `onTerminalAgentStatus` → `agentHookServer.ingestTerminalStatus`, `getAgentStatusSnapshot`, `getAgentProviderSessionSnapshot`, `getAgentProviderSessionRowsForPane`, plus `attestAgentHookCompatibilityAuthority` / `retireAgentHookCompatibilityAuthority` / `reconcileAgentStatusForEndedProcess` / `buildAgentHookPtyEnv`.
- `src/main/runtime/orca-runtime.ts:36844-36862` — `getHookRowsForPane` reads only those two injected fns; both are null in orcad.
- `src/main/runtime/orca-runtime.ts:37253-37257` — "only the hook payload" carries `providerSession`, so native chat has no transcript without it.

## Required outcome
- `session.tabs.listAll` (and `terminal.agentStatus`/`worktree.ps` where relevant) must surface hook-reported status and `providerSession.transcriptPath` on an orcad host, so a paired client can address the transcript.

## Suggested approach (optional)
- Mirror `src/main/index.ts:2884-2929` in the orcad runtime construction: at minimum `getAgentProviderSessionSnapshot: () => agentHookServer.getStatusSnapshot()` and `getAgentProviderSessionRowsForPane: (paneKey) => agentHookServer.getStatusSnapshotForPane(paneKey)`; the OSC bridge (`onTerminalAgentStatus`) and compatibility-authority seams should follow if they are needed on this host.
- Extend the orcad verification to assert a `providerSession.transcriptPath` is present in `session.tabs.listAll` for a pane that has POSTed a hook.

## Open questions for the human (optional)
- Is hook status supposed to reach clients through a different orcad-specific channel this PR intentionally leaves for a follow-up?

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. The read path from the prior review is now wired, matching the desktop host byte-for-byte.

Reviewed changes

  • Wired the agent-hook read seams — orcad-entry.ts now constructs OrcaRuntimeService with onTerminalAgentStatus, getAgentStatusSnapshot (filtered on providerSessionOnly), getAgentProviderSessionSnapshot (unfiltered), getAgentProviderSessionRowsForPane, attestAgentHookCompatibilityAuthority, retireAgentHookCompatibilityAuthority, and reconcileAgentStatusForEndedProcess, a 1:1 copy of src/main/index.ts:2884-2909.
  • Omitting buildAgentHookPtyEnv is justified — src/main/ipc/pty/host-env/assembly.ts:121 reads the agentHookServer singleton directly, so wiring a second source would be redundant.
  • Hardened the launch-failure path — armed stopOrcadAgentHookServer immediately after a successful start() so a later startup throw leaves the listener unbound; corrected the shutdown closure's await agentHookServer.stop() to the now-void agentHookServer.stop().
  • Pinned the wiring in a source-scan test — orcad-headless-startup-wiring.test.ts follows the existing src/main/startup/*.test.ts pattern and asserts graph-sentinel ordering, hook-server-before-daemon ordering, presence of all seven read seams, and the teardown/failure stop wiring.

ℹ️ Transcript delivery is pinned statically, not behaviorally verified

The read seams are correct, and the source-pin test will catch a regression that removes them, but nothing yet exercises the end-to-end claim — a POSTed hook's providerSession.transcriptPath surfacing in session.tabs.listAll (or terminal.agentStatus). The PR body's Verification section predates the read-path commits and covers only the write path (env vars, endpoint file, terminal creation). A follow-up that POSTs a hook in a scratch data root and asserts a transcript address in session.tabs.listAll would close the loop; not blocking, since the wiring that enables it is verified correct.

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

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 62a33194-cd88-415e-9459-a094ca674eb0

📥 Commits

Reviewing files that changed from the base of the PR and between 491f2dfaf49918829af645dcf990c3812d6a858f and 6328d76e1be199831ec7e6e567c2c3fe1a741692.

📒 Files selected for processing (2)
  • src/main/orcad/orcad-entry.ts
  • src/main/orcad/orcad-headless-startup-wiring.test.ts

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

Comment thread src/main/orcad/orcad-entry.ts Outdated
@klaidliadon

Copy link
Copy Markdown
Author

Pushed 090753f in response to review. One finding taken and broadened, one declined with reasoning.

Taken — CodeRabbit: "Stop the hook server when daemon teardown fails"

Correct, and the mechanism checks out. stopOrcadDaemon() → disconnectDaemon() → adapter.disconnectOnly(), and in daemon-pty-router.ts:297 that fans out with Promise.all over every adapter, while finishDisconnectOnly awaits an unguarded checkpoint write. The repo already models this rejecting — daemon-init-dependency-mocks.ts:141 exists to inject disconnectOnlyErrors.

I fixed it at the source rather than guarding the one line, because the hook server is not the worst thing the flat sequence drops. The steps after await stopOrcadDaemon() were:

agentHookServer.stop()
await browserProvider?.stop()
setRuntimeBrowserCommandsFactory(null)
runOrcadQuitHandlers()
instanceLock.release()

A daemon rejection skipped all five, and the last one is instanceLock.release() — a leaked lock file makes the next orcad launch refuse to start until someone deletes it by hand. That turns a transient teardown fault into a dead host, which is a good deal worse than a listener staying bound in a process that is on its way out. Guarding only the hook server would have left that in place.

Second await, same class: browserProvider.stop() ends in rm(sidecarDataPath, { recursive: true, force: true }) (electron-serve-browser-process.ts:188), and force forgives only ENOENT — EACCES/EBUSY/EPERM still reject. So both awaits are nested now and every later step runs, with the first failure still the one that propagates.

The launch-failure path in startOrcad had a third form of it that the comment did not reach: a rejecting browserProvider.stop() there replaced the error explaining why startup failed and skipped the lock release, so a launch that failed once could not be retried, and the operator got the symptom instead of the cause. That one catches and logs, then rethrows the original.

Pinned in orcad-headless-startup-wiring.test.ts, following the existing source-scan style in that file.

Declined — Greptile P1: "Retained PTYs Lack Hook Endpoint"

The mechanism is right. hook-stdin-contract.ts:46 is [ -n "${ORCA_AGENT_HOOK_ENDPOINT:-}" ] || return 0, so a PTY whose frozen environment never had that variable returns at the hook's first line forever. No argument there.

But there is no fix, and no regression:

  1. A running process's environment cannot be mutated from outside. The only remedy is recreating the terminal — which kills the user's running work, the exact property orcad's detached daemon exists to protect. Trading "agent status is late on pre-upgrade panes" for "your running builds die on upgrade" is the wrong trade.
  2. It is strictly an improvement over the current state. Before this PR the server never started, buildPtyEnv() returned {}, and zero PTYs had hook coordinates — measured, 0/6 on the live agent process. After it, every newly spawned PTY has them. The affected population is only PTYs spawned by an orcad build that predates the feature, on a host upgraded in place, and it drains as those terminals are recreated.
  3. The adjacent case that recurs forever is handled. A PTY that has the variable but stale port/token happens on every single restart, because start() regenerates both while the daemon deliberately outlives the runtime. That is why userDataPath is passed and the endpoint file is written — the hook re-reads coordinates at invocation time. That is the case worth engineering for, and it is the one the PR engineers for.

So: accurate observation, one-time transient, unfixable in this direction.

Pullfrog: "pinned statically, not behaviorally verified"

Agreed, and the PR body already says something stronger — see the "⚠️ Commit 3 is necessary but not sufficient" section. The hook server does hold rows for both live panes with transcriptPath populated, and session.tabs.listAll still reports no providerSession, so an end-to-end assertion would currently fail. The suspect named there is the merge's recency gate (hookAgentStatus.providerSessionReceivedAt ?? -1) >= tab.agentStatus.updatedAt, which the wiring in commit 3 makes reachable for the first time. That is flagged as a hypothesis from reading the condition, not a measured result, and it needs an instrumented build to settle.

That section is deliberate and is the next person's starting point — please don't trim it in review. The wiring here is correct and required either way; I'd rather land it and say so plainly than imply chat is fixed.

Gates

tsc/oxlint/pnpm test are not installable on the host this was developed on — CI is the authority. What I did verify locally: both edited files parse, and I re-ran the source-scan test's string assertions (new and pre-existing) against the edited source, 19/19.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor comment-accuracy note inline.

Reviewed changes

  • Reworked the launch-failure path — browserProvider.stop() in startOrcad's catch is now guarded, so a teardown rejection can't skip instanceLock.release() or mask the original startup error.
  • Nested the shutdown teardown — stop() now runs each teardown step even when a prior await rejects, guaranteeing the instance lock is released.
  • Pinned the structure in a test — the new test asserts the try/finally nesting and the guarded failure path.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/orcad/orcad-entry.ts Outdated
@klaidliadon

Copy link
Copy Markdown
Author

Pushed 3dd8f20. One correction taken, one check declined.

Taken — Pullfrog: the "first failure" claim is inverted

Correct, and I got this wrong in both the code comment and my previous reply here. A throw from a finally block replaces the in-flight exception, so the last failing step wins, not the first. Verified rather than assumed:

try { throw new Error('A-daemon') }
finally {
  try { throw new Error('B-browser') }
  finally { console.log('lock released: yes') }
}
// lock released: yes
// propagated: B-browser

So if the daemon and the browser provider both reject, the caller sees the browser's error and the daemon's is lost.

I've reworded the comment to state the guarantee the code actually provides — every step runs — and to say plainly which error survives. I did not add error aggregation to preserve both: the lock is released either way, and a lost second error costs a log line where a skipped release costs the next launch. That is the trade worth making, but it should be written down rather than papered over with a wrong claim, which is what the comment was doing.

Comment-only change; no behaviour difference from 090753f.

Declined — CodeRabbit "Linked Issues check": fix the provider-session merge in this PR

The warning reads: "the description confirms that session tabs still lack provider-session data, so the agent chat objective is not yet fully met", resolving to "investigate and fix the downstream provider-session merge... then add behavioral coverage."

Not in this PR, for three reasons:

  1. The check is firing because the PR is honest. That warning exists only because the description says out loud that session.tabs.listAll still reports no providerSession. Deleting the ⚠️ section would turn this check green without changing a line of behaviour. That is precisely why it stays.
  2. The suspected cause is a hypothesis, not a diagnosis. The recency gate (hookAgentStatus.providerSessionReceivedAt ?? -1) >= tab.agentStatus.updatedAt is suspect from reading the condition. Wiring the accessors makes it reachable for the first time, so it has never executed in this host before. "Fix it" without an instrumented build that confirms it is where the failure lives is a guess dressed as a fix, and a wrong guess here lands silently — the same failure mode that produced all three omissions this PR repairs.
  3. The scope is the three startup omissions, and they are independently correct. Terminal creation goes from runtime_unavailable to working, listAll goes from hanging forever to answering, PTY hook env goes from 0/6 to 6/6. Holding that behind an unrelated downstream bug helps nobody.

The behavioral coverage the check asks for would currently fail — that is the point of the caveat. Adding a test that asserts a transcript reaches the chat view would be asserting a thing that does not happen yet.

Not addressed — Docstring Coverage (33.33%)

Unchanged by my commits: it read 33.33% across the same 3 functions before them. This file's convention is dense inline // Why: rationale plus a file-level docstring, which both changed files carry; adding JSDoc headers to satisfy a percentage would add words without adding information. Deferring to a human maintainer if the repo actually wants that threshold enforced here.

Still open — Greptile: retained PTYs

Greptile's updated summary now states the mechanism itself: "daemon adoption reuses existing processes whose environments cannot be mutated." That is the reason it cannot be fixed from this direction, and it is the same point I answered above — one-time transient affecting only PTYs spawned before the feature existed, draining as terminals are recreated, against a baseline where no PTY had hook coordinates.

@enieuwy

enieuwy commented Sep 23, 2026

Copy link
Copy Markdown

Independent reproduction of #17846 on current code, and confirmation that the graph publication in this PR is the fix for the mobile terminal path.

Setup: orcad built from the v1.4.209 tag (pnpm run build:orcad, Node 24.21.0) on Debian 13 x86_64, --bind <lan-ip> --port 6768, scratch data root. Client: the built CLI's RuntimeClient with a runtime-scope pairing code, calling the RPCs directly.

Unpatched v1.4.209:

terminal.create (CLI, the path smoke:orcad-terminal gates)   ok
session.tabs.createTerminal                                  runtime_unavailable (133 ms)
session.tabs.list                                            ok
session.tabs.listAll                                         no response, client timeout at 10 s

Same build plus only the graph line from this PR (runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] }) after reconcileLegacyWorkerTerminals()):

session.tabs.createTerminal   ok (268 ms)
session.tabs.list             ok
session.tabs.listAll          ok (31 ms)
PTY round trip via new tab    ok

A terminal created before two orcad restarts still round-tripped afterwards (daemon adopted, same pid), so the graph publication does not disturb daemon adoption.

Two notes that may help this land:

  • v1.4.209 still has no syncWindowGraph call anywhere under src/main/orcad/, so every paired phone on an orcad host still gets runtime_unavailable on terminal creation.
  • GitHub reports this branch as conflicting with main. main now starts agentHookServer in orcad-entry.ts on its own, which looks like the overlap. If a rebase is heavy, the graph publication alone is enough for the mobile terminal path, so it could land separately from the hook work.

orcad never publishes its window layout. The runtime treats that layout as the thing it places work into, so until something publishes one the runtime reports itself unavailable and refuses everything that depends on it. Creating a terminal is refused before it reaches any terminal code, and listing tabs waits on a publication that can never arrive. The daemon is healthy throughout, it is simply never asked.

This publishes the same empty placeholder the Electron serve path publishes, after terminal recovery so the layout is adopted against a settled runtime, and before the transport binds so no client can observe the unavailable state.

Fixes stablyai#17846

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PHbhxM6H9evtLR95vUGs4G
@klaidliadon
klaidliadon force-pushed the fix/orcad-headless-graph-and-agent-hooks branch from 1a18599 to b65bf8c Compare October 1, 2026 09:16
@greptile-apps

greptile-apps Bot commented Oct 1, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Published the headless graph — runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] }) now runs after reconcileLegacyWorkerTerminals and before the RPC server binds (orcad-entry.ts:297), mirroring main-process-runtime-launch.ts:160 so graphStatus reaches 'ready' and the graph-gated terminal-create / tab-inventory RPCs unblock.
  • Pinned the ordering in a source-scan test — headless-pty-hydration-ordering.test.ts:56-68 asserts the sentinel sits after registerHeadlessPtyRuntime and before new OrcaRuntimeRpcServer(.

ℹ️ The description no longer matches the diff

The PR body narrates five commits — the agent-hook server start, the seven read seams, the failure/teardown hardening — but the diff is now a single commit adding only the graph publication. That other work is already on main (verified against origin/main's orcad-entry.ts), so nothing is missing from the host; it is the description that overstates the scope. Worth narrowing before merge so a reviewer does not approve expecting the hook-server wiring to land here.

Technical details
# Align the PR description with the diff

## Affected sites
- `src/main/orcad/orcad-entry.ts:295-297` — the syncWindowGraph call is the only production change in the diff.
- `src/main/startup/headless-pty-hydration-ordering.test.ts:56-68` — the only test change.
- `origin/main:src/main/orcad/orcad-entry.ts` — already contains `agentHookServer.start(...)`, all seven read seams, `buildAgentHookPtyEnv`, and the nested teardown.

## Required outcome
- The description's "What Changed" and "Known limitations" reflect what this PR actually ships (the graph publication), or the broader work is explicitly marked as already merged to `main`.

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

@OrcaWin

OrcaWin commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

Thank you for spotting these gaps. Both are covered on main now: the runtime has a headless graph fallback so orcad terminals work (Phase 3, #24863, where every SSH host runs orcad), and orcad starts the agent hook server and installs hooks on startup (#27319). Closing as fixed.

@OrcaWin OrcaWin closed this Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: orcad cannot create a terminal at all, and its agent chat renders nothing

3 participants