Skip to content

fix(chat): show a Windows Claude conversation started in a terminal - #2903

Merged
devswha merged 5 commits into
mainfrom
fix/windows-claude-chat
Nov 5, 2026
Merged

devswha merged 5 commits into
mainfrom
fix/windows-claude-chat

Conversation

@devswha

@devswha devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Fixes #2896

What was wrong

On Windows, herdr-core's terminal.get names the pane's shell (pwsh.exe) as the foreground even while Claude Code runs in it (Windows has no foreground process group), and herdr-core reports no session for Claude. Chat looks for the Claude process in the pane's foreground to read its native PID record (~/.claude/sessions/<pid>.json); with none found it had no session id and fell back to the terminal: GET /api/pane/conversation answered {"source":"scrollback","turns":[]}.

Fix

When no Claude is in front on Windows, Chat now takes the Claude below the pane's shell from the process table, the same lookup the session list already used for the store. That helper moves from session-history-routes.ts to claude-store.ts (windowsShellClaude) and both callers use it. The process table read is the existing 5 s cached one.

Validation

  • Failing first: the new conversation.test.ts case (finds the Claude below a Windows pane's shell and its session by the PID record) fails on main (found is null) and passes with the fix.
  • Focused: server/conversation.test.ts, claude-store.test.ts, claude-session.contract.test.ts, session-history.test.ts, server/core/ pass; bun run typecheck exit 0.
  • bun run check fast: all green (11627 pass, 0 fail).
  • Windows 11 VM, herdr-core 0.1.17, Claude Code 2.1.318, a pwsh terminal running Claude:
    • server from main: /api/pane/conversation?pane_id=t1 -> {"source":"scrollback","turns":[]} (reproduced).
    • server from this branch, same terminal: source: "claude-transcript" with the turns, and a message sent from the Chat composer shows its reply (PONG-CHAT3) in Chat.
  • Not run: check full and the Windows installer build (CI's Native Windows install covers the latter).

herdr-core names only the shell in front on Windows and reports no session for Claude, so Chat found no Claude process and fell back to the terminal. Take the Claude below the pane's shell from the process table, as the session list already did.
@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Explain Change →

Caution

Review failed

The pull request is closed.

Summary

Summary by CodeRabbit

  • Bug Fixes
    • On Windows, Chat now displays Claude Code conversations started in a desktop app terminal instead of showing “Conversation unavailable.” These conversations are also recognized when viewing session history, making them accessible alongside conversations started through other supported workflows.

On Windows, the server now searches a pane shell’s descendants for Claude processes when foreground process data does not identify Claude. The conversation and session-history paths use this lookup. A Windows test checks the conversation transcript path and PID.

Changes: Windows Claude conversation discovery

Layer / File(s) Summary
Shared Windows shell process lookup
server/claude-store.ts
Adds windowsShellClaude to read the process table and return matching Claude descendants of a shell.
Conversation and session-history integration
server/conversation.ts, server/session-history-routes.ts, server/conversation.test.ts, changelog.d/windows-claude-chat.md
The conversation path uses the shell lookup on Windows when foreground process data contains no Claude and a numeric shell PID is available. The session-history route uses the shared lookup instead of its local helper. The test checks the child Claude process PID and transcript path; the changelog records the Windows Chat fix.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Possibly related PRs

  • devswha/ovis#1521: Updates Claude process recognition used by the new Windows descendant lookup.
  • devswha/ovis#2572: Maps the runtime’s claude-code status to the server’s claude agent, which is used in the same Claude Chat path.

Suggested reviewers: haeminway1, jpcarranza94

Merge Risk: Low · up to 917b5

The Windows discovery fix is mergeable with awareness of two edge cases: nested Claude sessions can show the outer conversation, and rapid PID reuse can temporarily prevent conversation discovery. npm-installed Claude remains an existing unsupported discovery case.

- [ ] Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Review notes (omo: CLEAR at 70a860b; Codex adversarial review: no HIGH, four MEDIUM).

Fixed in 27775a5:

  • The new test set HOME but not USERPROFILE, which userHome() prefers on Windows, so on a real Windows run it would have read the real profile. It now isolates and restores both (checked with USERPROFILE=/nonexistent).

Not changed here (all reached through the existing Windows lookup that the session list already uses; none reproduced as a user-facing regression of this PR):

  • A Claude nested below another Claude in the same pane (Claude A -> shell -> Claude B) picks the outer one; the process tree cannot tell which one holds the terminal. Before this PR such a pane showed no conversation at all on herdr-core.
  • A Claude installed through npm on Windows runs as node.exe ...\cli.js; isClaudeProcess checks only argv[0], so the fallback does not find it (same as before). The native installer, which Claude Code recommends on Windows, is found.
  • The per-process store cache is keyed by pid and argv for 30 s; a Claude started in another store under a reused pid within that window could read the old store until the cache expires.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Deploying ovis with  Cloudflare Pages  Cloudflare Pages

Latest commit: 917b5df
Status: ✅  Deploy successful!
Preview URL: https://590a3dce.ovis.pages.dev
Branch Preview URL: https://fix-windows-claude-chat.ovis.pages.dev

View logs

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Merge note: Windows-specific fix; the Native Windows install check passed on GitHub and the fix was verified in the Windows VM (Chat shows Claude's reply). The browser failures in its CI run are main-inherited (terminal-arrows-clicks from #2798, fixed by #2932; workspace-touch-reorder timeout also on main), checked script by script. check fast green at this head, omo CLEAR.

@devswha
devswha merged commit 6236d2e into main Nov 5, 2026
15 of 19 checks passed
@devswha
devswha deleted the fix/windows-claude-chat branch November 5, 2026 08:46
devswha added a commit that referenced this pull request Nov 5, 2026
… tab (#3023)

Fixes #3009

## What was wrong
On herdr-core (macOS and Linux), `terminal.get` reports only the first
process of a terminal's foreground job as its `foreground`; herdr
reports every process of the job. Chat looks for the Claude process in
front of the pane to read its native PID record
(`~/.claude/sessions/<pid>.json`), and herdr-core reports no session for
Claude. So two common setups fell back to the terminal (`GET
/api/pane/conversation` -> `{"source":"scrollback","turns":[]}`),
although hc labels both terminals `app=claude-code`:

1. Claude Code started by a launcher script: the foreground is the
launcher (`bun launch.js`), Claude is its child.
2. A tab running `hc attach <t>`: the foreground is `hc attach t5`,
Claude runs in `t5`.

## Fix
Only on a server serving herdr-core, and only when no Claude is in front
(the Windows path from #2903 is unchanged):

- **Launcher:** the processes below the foreground process and in its
process group (what the terminal's keyboard reaches) are read from the
kernel: `/proc/<pid>/task/*/children`, `stat` and `cmdline` on Linux;
sysctl `KERN_PROC_PGRP` and `KERN_PROCARGS2` on macOS
(`darwin-process.ts`, in the server's own thread, no `ps`). New module
`server/foreground-job.ts`. Exactly one Claude there is taken; zero or
two or more bind nothing, so the pane keeps the terminal as today.
- **`hc attach <t>`:** when the foreground is `hc`/`herdr-core` with
exactly `attach <word>`, the Claude of that terminal is used, only when
all hold: the client process reaches this server's core (its socket,
worked out from the client's own environment as hc does it,
`discovery.ts` `coreSocket`, is the socket of the core the server's `hc
connect` reaches; an environment that cannot be read follows nothing),
since terminal numbers repeat in every core; hc's own
`pane.process_info` answer for the word reports that same address (a
name hc resolves to another terminal, or another machine's `x/t1`, is
not followed); and only one hop.

The PID record still has to match the process's start (`/proc` ticks,
`ps -o lstart` on macOS), as before.

## Validation
- Failing first: the new `conversation.test.ts` cases `finds the one
Claude a launcher runs, and its session by the PID record` and `shows
the conversation of the local terminal an \`hc attach <t>\` tab views`
fail on main (`found` undefined) and pass with the fix. The fail-closed
cases (two Claudes below; an attach word hc resolves to another address;
an attach client that reaches another core) were checked to fail when
their guard is removed. They use real stand-in processes (argv[0]
`claude`) and real PID records, with only the herdr RPC mocked.
- `server/foreground-job.test.ts` reads the real kernel: passes on Linux
and on macOS (eamac, macOS 27.0.1 arm64, fresh `/tmp` reference clone).
- Focused: `server/conversation.test.ts`, `claude-store.test.ts`,
`claude-session.contract.test.ts`, `session-history.test.ts` (412 pass);
`bun run typecheck` exit 0.
- Live, isolated hc 0.1.18 core (own `HC_SESSION`, state/config/runtime
folders), Ovis with `OVIS_RUNTIME=herdr-core`, temp `HOME`, telemetry
off, a Claude stand-in that reports OSC 7501 `app=claude-code` and
writes a PID record and transcript:

  | terminal | main | this branch |
  |---|---|---|
| shell -> `bun launcher.js` -> Claude | `scrollback` |
`claude-transcript` (its turns) |
  | Claude directly | `claude-transcript` | `claude-transcript` |
| shell -> `hc attach <that terminal>` | `scrollback` |
`claude-transcript` (the viewed terminal's turns) |
  | Claude stand-in without a PID record | `scrollback` | `scrollback` |
| shell -> `HC_SOCKET=<a second core> hc attach t8` (that core's t8 runs
another Claude; this core's t8 runs one too) | `scrollback` |
`scrollback` |

Same result on Linux (P620) and on macOS (eamac, hc 0.1.18 macOS build;
the second-core row on Linux only).
- `bun run check fast`: all green after the review fix and a merge of
main (12197 pass, 0 fail; run on old-desktop through the heavy runner).
- Not run: `check full`, and a real signed-in Claude Code (the stand-in
writes the same PID record format).

## Review round
omo + Codex (Terra, xhigh) on 9e8cb69: one reproduced HIGH, an `hc
attach` client pointed at another core (`HC_SESSION`/`HC_SOCKET`) bound
this core's same-numbered terminal. Fixed in 464f4a5/42da5b4 as above,
with a test that fails without the check, and checked live with two
cores.
devswha added a commit that referenced this pull request Nov 5, 2026
… PID record (#3047)

## Change

On Windows, herdr and herdr-core name only a pane's shell in front, so
`windowsShellClaude` (#2903) takes Claude from the process table: the
nearest one below the shell, and any other not started by it. Claude
Code's native build can run the session in a child `claude.exe` of the
`claude.exe` the shell started, and only that child writes the PID
record (`~/.claude/sessions/<pid>.json`). Seen on a real Windows 11 PC
with herdr-core and Claude Code 2.1.304:

```text
powershell.exe -> claude.exe (no options, no record) -> claude.exe --permission-mode ... (the record)
```

The lookup takes the launcher, which names no session and no store:

- Chat: `GET /api/pane/conversation` answers
`{"source":"scrollback","turns":[]}`.
- Earlier sessions: 409 `claude_store_unknown`.
- Resume: a Claude that comes up on such a chain has its new tab closed,
409 `launch_store_unknown`.

Now, of two or more Claudes below the shell, #2903's rule runs over
those holding a live PID record in a store Claude may use, once one
does. Both callers share the lookup, so Chat and Earlier sessions get
the same process.

| Claudes below the shell | Before | Now |
| --- | --- | --- |
| None, or the table says nothing | `[]`, `null` | Same |
| One, with or without a record | That one | Same, and no store is read
|
| Launcher and child, no record yet | The launcher: no session, store
unknown | Same |
| Launcher, and a child holding the record | The launcher | **The
child** |
| A holder, and a Claude beside it with no record | Both: refused |
**The holder** |
| Two holders, neither started by the other | Both: refused | Same |
| A holder, and a Claude it started that holds one too | The nearest one
| The outer holder |

A Claude with no record names no session, so the one beside it with a
record is the only session there is to show; if the other is still
starting, the answer turns to refused once it writes its own. A record
that cannot be read, or that two stores hold, may be the live one, so
its process counts as a holder, and the callers still refuse that
process's store (`AmbiguousClaudeStore`, #586).

This is Windows only, as #2903 is: both callers reach
`windowsShellClaude` only on `win32`, so Linux and macOS run none of it.
#3023 fixed a launcher on macOS and Linux by reading the foreground job
from the kernel; on Windows, where hc names only the shell, the process
table and the PID record are what there is.

Known limits:

- An outer Claude that is itself the session but whose record cannot be
read as live at that moment (torn, say), with a `claude -p` or SDK child
below it that holds a record, now resolves to the child, where main
finds no session. A live session's record is rarely rewritten (none in 8
minutes of watching one), so this is rare.
- A `claude attach` client keeps no PID record: with one in front and a
holder beside it, the holder's session is shown, where main refuses.
Both must run under one shell.

## Validation

- Failing first: the final tests on main's source (`0011d8fd`) fail 8
tests on Linux and on Windows: `server/conversation.test.ts` "finds a
Windows pane's session by the child claude.exe holding the PID record,
not the one its shell started" (`found` is null),
`server/session-history.test.ts` "lists the store of the child Claude
holding the PID record, not the launcher's" (`claude_store_unknown`) and
"keeps a resume from a shell pane that came up on ~/.claude, and closes
one on another store" (`[409, "launch_store_unknown", ["t-new"]]`), and
5 in `server/claude-store.test.ts`. All 8 pass with the fix.
- Linux (WSL Ubuntu, Bun 1.4.2, clean checkout of this commit on
`5b51cbe3`): `bun run typecheck` passes; `server/claude-store.test.ts`,
`server/conversation.test.ts`, `server/session-history.test.ts`,
`scripts/changelog-fold.test.ts`: 420 pass, 2 skip, 0 fail.
- Windows 11, native (Bun 1.4.2), on this commit's rebase on `f2bbee9e`:
`server/claude-store.test.ts` and `server/session-history.test.ts`: 156
pass, 43 skip, 6 fail; the same 6 fail on main on that PC (`EPERM`
making a symlink, SQLite). `bun run typecheck`: exit 0.
- Linux and macOS paths: the diff outside `windowsShellClaude` moves one
`const home` above a `win32`-only block and passes it in. Tests set the
platform to `linux` and `darwin`: a shell-only pane reads no process
table, in Chat and in Earlier sessions.
- Read-only dry run on a real PC (the real process table and the real
PID records, no hc command, no socket, no server), for each shell with a
Claude below it:

  | Shell | Claudes below it | Before | Now |
  | --- | --- | --- | --- |
| `powershell.exe` 34500 | 15056 `claude.exe` (no record) -> 3760
`claude.exe --permission-mode` (record in `.claude`) | 15056 | **3760**
|
| `powershell.exe` 18424 | 46128 `claude.exe` (record in `.claude`) |
46128 | 46128 |

  The lookup took 0.9 ms and 0.1 ms with the table in hand.
- Everything together on GitHub's hosted machines, this branch merged
with the other open Windows herdr-core branches on `f2bbee9e`, on my
fork: upstream's `ci.yml`
([run](https://github.com/photojupi/ovis/actions/runs/42078075843)):
`bun run check fast` passes (unit: 12253 pass, 0 fail, 39 skip), and so
do Integration tests, every Browser regression shard, Rust PTY host, the
herdr-core live contract and Native Windows install. That job's `Release
guards` step fails on a fork with no `v*` tags; with the tags it says
this is no release. `Native macOS session identity` got no `macos-15`
runner, twice; its test files ran in the whole unit suite below, with no
failure among them.
- macOS: a real hc 0.1.17 and the real server on GitHub's `macos-14` and
`macos-26` (arm64) and `macos-15-intel`, with a stand-in agent:
`.github/platform-check/e2e.ts` on my fork gives 19 ok, 0 failed on each
([arm64
run](https://github.com/photojupi/ovis/actions/runs/42084111704), [Intel
run](https://github.com/photojupi/ovis/actions/runs/42078075706)): a
workspace opens, the terminal takes typing through the API and the
browser, the agent starts with `it's`, `$HOME`, `a\b`, `"q" r`, an empty
argument and `한글 인자` intact, and closing leaves nothing.
`server/herdr-core/live.test.ts` on that hc: 7 pass, 1 fail, the schema
test, which pins hc 0.1.14 as `ci.yml` does and fails without these
branches too. The whole unit suite on `macos-15-intel`, all these
branches merged against their base: the base's 41 failures are the same
on both sides (`tar --transform`, workflow files and the like, none in a
file these branches change but `server/session-history.test.ts`, whose
failures match by name); a first, slower run's two extra timeouts, in
files these branches do not touch (`server/pi-delivery.test.ts`,
`server/windows-native.test.ts`), did not come back on the rerun. No
real MacBook: the Herdr app's own window on macOS was not checked.
- Not run: `bun run check full` as one command; Chat, Earlier sessions
and Resume end to end against a running herdr-core with a real Claude on
such a chain (the dry run stops at the lookup).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: devswha <devswha@gmail.com>
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.

Windows desktop app: Chat shows 'Conversation unavailable' for Claude Code, whose replies only show in the terminal

1 participant