Skip to content

fix(chat): show Codex chats whose stored Windows paths are prefixed - #582

Merged
devswha merged 5 commits into
devswha:mainfrom
David-Sousa-Web:fix/windows-codex-path-prefix
Oct 7, 2026
Merged

devswha merged 5 commits into
devswha:mainfrom
David-Sousa-Web:fix/windows-codex-path-prefix

Conversation

@David-Sousa-Web

Copy link
Copy Markdown
Contributor

Part of #518: this fixes its first two points (the \\?\ prefix on rollout paths and on threads.cwd). The third point, a pane resumed through the codex resume picker, is left for a separate PR, so this does not close the issue.

Change

On Windows, Codex stores its paths in state_5.sqlite with the \\?\ prefix, as Rust's canonical Windows paths carry it. herdr reports the plain form. Measured on Windows 10 with herdr 0.9.3 and codex-cli 0.160.1, against a real store: 504 of 505 threads rows have a prefixed cwd, and 13 have a prefixed rollout_path.

That breaks both ways the server finds a Codex pane's rollout:

  1. The thread herdr names for the pane. Its rollout_path is \\?\C:\Users\…\.codex\sessions\…\rollout-….jsonl. realpathSync keeps the prefix, so relative() against the store's C:\Users\…\.codex\sessions puts the file outside the store, and codexRolloutPath refuses it.
  2. The threads of the pane's cwd. herdr reports D:\work and the row says \\?\D:\work, so WHERE cwd = ? finds none of them.

The pane then ends at no_session_path and Chat shows Conversation unavailable.

The fix, in server/codex.ts:

  • withoutVerbatimPrefix turns \\?\D:\x into D:\x and \\?\UNC\host\share into \\host\share. Any other path is returned unchanged.
  • codexRolloutPath strips the prefix before realpathSync, then applies the same checks as before: canonical path, inside <codexHome>/sessions, a .jsonl file, and not a subagent's rollout. A prefixed path outside the store is still refused.
  • The two cwd queries (newerThreads and the candidate list in codexTranscriptPath) match cwd IN (?, ?) with both stored forms from storedCwds. A POSIX cwd is passed twice unchanged, so on Linux and macOS the queries return the same rows as before.

Nothing new reaches the browser, there is no contract change and no new dependency.

Validation

Live, on Windows (a real Codex pane in herdr, server environment as plugin.ps1 sets it). The pane was started as codex resume <id>, and herdr reports that thread as its agent_session:

main this branch
paneConversation no_session_path → Conversation unavailable codex-transcript, 45 turns
interactive threads found for the pane's cwd 0 3

Linux (Ubuntu 24.04 under WSL, Bun 1.4.2, Node 24, a clean clone of this branch):

  • bun run generate:types --check, bun run typecheck and bun run build pass.
  • bun run test:unit: 1717 pass, 6 skip, 0 fail.
  • HERDR_TEST_MODE=unit bun test on codex.test.ts, codex-processes.test.ts, conversation.test.ts, windows-native.test.ts and release-notes.test.ts: 148 pass, 5 skip, 0 fail.
  • With main's server/codex.ts, the new codex-processes.test.ts case fails, and the other 29 in that file pass.

Windows (Bun 1.4.2):

  • windows-native.test.ts: 5/5, including the new case.
  • With main's server/codex.ts, the new case fails with null, the same error as the live pane.
  • codex.test.ts: the new case passes. The one failure there is the existing symlink case (EPERM without Developer Mode), which fails the same way on main.

Tests added:

  • server/codex.test.ts: withoutVerbatimPrefix and storedCwds for drive, UNC, POSIX, relative and device paths.
  • server/codex-processes.test.ts: a pane whose cwd is D:\work\app finds the thread Codex stored under \\?\D:\work\app. This runs on any OS, through the real codexTranscriptPath with the existing fake herdr socket.
  • server/windows-native.test.ts (real Windows only, like the other tests there): a \\?\-prefixed rollout path inside the store resolves to the plain canonical path. A prefixed path outside the store, or one that climbs out with .., is still refused.

I did not run test:integration or test:ui locally.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: devswha/herdr-web-ui/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 25c4d045-c1f7-486e-b9ad-053737746689
📥 Commits

Reviewing files that changed from the base of the PR and between 4e3eac6 and b993574.

📒 Files selected for processing (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

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


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Fixed Codex chat availability on Windows when session paths use the \\?\ prefix. Matching conversations now appear whether the path includes that prefix or not, including when the saved session path and requested path use different forms. Paths outside the Codex store continue to be excluded, so this fix does not make unrelated files available as Codex sessions.

Walkthrough

Codex path handling now normalizes supported Windows verbatim paths during rollout resolution. Thread queries check both plain and verbatim working-directory forms. Tests cover path normalization, rollout boundaries, and transcript matching.

Changes

Windows Codex path handling

Layer / File(s) Summary
Normalize Windows verbatim paths
server/codex.ts, server/codex.test.ts, server/windows-native.test.ts
Path helpers remove supported Windows verbatim prefixes, and rollout resolution uses the normalized path. Tests cover drive, UNC, and other path forms, plus rollout paths inside and outside the store.
Match stored working-directory variants
server/codex.ts, server/codex-processes.test.ts, CHANGELOG.md
Thread queries match either the plain or verbatim working-directory form. A regression test checks transcript lookup, and the changelog records the Windows Codex case.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to b9935

This change adds a changelog note about Windows Codex path handling. No merge-blocking risk was identified in the supplied change.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 4e3ea

Normal Windows path resolution retains its containment checks. However, an existing fallback becomes reachable for additional database rows and can read outside the session store. This requires control of those rows and specific pane conditions; direct disclosure or increased privileges were not established.

Retained concerns

  • Low · security · inferred: Dual-form cwd matching expands access to a pre-existing unvalidated-path fallback. An eligible newer-thread row with an outside-store rollout_path survives failed containment validation and can reach filesystem reads when a resumed or previously bound pane needs cross-pane ownership inspection. The attacker would need influence over the selected database rows; remote reachability and privilege gain are unestablished.
Security review details

Security Blast Radius

  • inferred — With control over an eligible alternate-form cwd row, a principal can nominate an outside-store path for server-side ownership inspection. Potential file access is bounded by the server account's permissions, not the sessions directory. Production database-write authority and cross-user or remote exposure were not established.

Security Findings and Attack Paths

  • inferred — The conditional path is alternate-form cwd row to newerThreads raw fallback to cross-pane claim inspection to history-file reads. It requires an eligible newer thread, resumed or cached-binding resolution, no successful screen match, and another same-cwd Codex pane. Normal display candidates still exclude rejected paths, so direct browser disclosure was not established.

Trust Boundaries and Controls

  • observed — The changed SQL queries remain parameterized. Direct rollout resolution preserves canonical containment, while the newer-thread fallback replaces a failed validation result with the original database path before ownership inspection.

Hardening Proposals

  • proposed — Separate unresolved-thread blockers from validated readable rollout paths. Missing or outside-store rows should still prevent uncertain fallback to an older conversation, without allowing those paths to be opened or cached as readable ownership candidates.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: displaying Codex chats when Windows-stored paths include the verbatim path prefix.
Description check ✅ Passed The description includes the required Change and Validation sections. It explains the problem, implementation, scope, test coverage, results, and unrun checks. It also clearly states that the remainin…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 4 files. (1 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

A \\?\Volume{...} or \\?\GLOBALROOT path without its prefix reads as a
relative path and resolves against the server's working directory, so it
keeps the prefix. The Windows-only traversal case now climbs with raw ..
segments to a file that exists, instead of a missing one.

@devswha devswha left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks @David-Sousa-Web, this is a real fix for the first two points of #518. I reviewed it, pushed two small commits on top (plus merges of main), and am merging it. #518 stays open for the codex resume picker.

What I checked

  • The fix itself. Both lookups that failed now work: codexRolloutPath strips \\?\ before realpathSync, and both cwd queries match cwd IN (?, ?). The new codex-processes.test.ts case fails against main's server/codex.ts (1 fail, 29 pass) and passes with the PR.
  • Containment. A stripped path still has to resolve inside <CODEX_HOME>/sessions. Scratch tests confirmed that a prefix that leaves a relative path is still refused when the server's cwd is outside the store.
  • Cross-binding. An in-memory threads table matched D:\work\app only to D:\work\app and \\?\D:\work\app. A sibling (app2), a child, the parent, another drive, \\?\UNC\D:\... and POSIX /work/app were never returned. UNC (\\host\share\app) matched only its own \\?\UNC\ form.
  • Linux/macOS. storedCwds gives a POSIX cwd twice, unchanged, so the query returns the same rows. codexRolloutPath returns the same results as main on POSIX inputs, including file names that contain backslashes. Case and forward-slash variants (d:\work, D:/work) are never mapped to another directory.
  • Second opinion. I ran an adversarial review with Codex (gpt-6.1-sol, xhigh). It found no High issues and I verified each finding.

What I changed on the branch

  • 20fbdda strips \\?\ only from drive (X:\) and UNC\ paths. Before this, \\?\Volume{GUID}\... or \\?\GLOBALROOT\... lost its prefix and became a relative path, which realpathSync then resolved against the server's working directory. Those forms now stay as they are, and codex.test.ts covers them.
  • 20fbdda also fixes the Windows-only traversal assertion. join() folded the .. segments, so it pointed at a file that did not exist and passed through ENOENT. It now climbs with raw .. segments to other.jsonl, which exists.
  • 4e3eac6 adds the changelog link and credit, as ([#582](...) by @David-Sousa-Web).

Left as follow-ups (not blocking)

  • Spelling mismatch between the two cwds. If herdr's cwd and Codex's stored cwd differ in case or separator (d:\work vs \\?\D:\work), neither candidate matches. After /new, the pane can then keep showing its older thread, but never one from another directory. Matching Windows directories case-insensitively would cover this, and it fits a separate PR together with the resume-picker point.
  • Pre-existing containment edge case. path.win32.relative folds case, so in a per-directory case-sensitive folder a SESSIONS directory next to sessions would pass the store check. Plain paths already behaved this way before this PR.

What ran and what did not

  • Ran. HERDR_TEST_MODE=unit bun test on codex.test.ts, codex-processes.test.ts, windows-native.test.ts and conversation.test.ts: 130 pass, 5 skip, 0 fail. bun run check fast passed on 4e3eac6 (the branch merged with main at 90536be): 1717 pass, 6 skip, 0 fail. The same files plus scripts/plugin.test.ts pass on b993574, which also merges #577. CI (Fast checks, Integration and browser, Native Windows install) is green on b993574.
  • Not run. The five windows-native.test.ts cases, including the new prefix case and my traversal fix, skip on Linux. I relied on your Windows run and on the Native Windows install job. I did not test with a live Windows Codex pane.

@devswha
devswha merged commit c04c760 into devswha:main Oct 7, 2026
12 checks passed
devswha added a commit that referenced this pull request Oct 8, 2026
## Change

Prepares v0.4.1 and remote bundle 21.

- `package.json` and `herdr-plugin.toml`: 0.4.0 → 0.4.1. A patch
release: fixes from the audit and the pre-release reviews (#566, #570,
#588, #592), Windows chat discovery (#563, #582), the plugin's port
handling (#577), GJC menus (#593) and more. The new pieces are small and
opt-in or additive: the sidebar's Activity order and Quiet opened
finishes (#529, both off by default), the `/effort` card (#594), the
clipboard-from-a-pane switch (#570) and the Portal guide (#229).
- `shared/machines.ts`: `REMOTE_BUNDLE_VERSION` 20 → 21. `server/` code
that ships in the bundle changed since v0.4.0 (#566, #563, #572, #576,
#582, #583, #588, #519, #570, #592, #593, #594), so remote PCs need a
new bridge.
- `CHANGELOG.md`: the Unreleased notes become `[0.4.1] - 2026-10-08`,
with the compare links. #592 merged without entries; its three
user-visible fixes are added under Fixed (forwarded-address token limit,
https-only web push that never follows a redirect, Tab and Escape in Add
PC over a file preview).
- `release-summaries.json`: 0.4.1 in English, Korean, Japanese and
Chinese: four new, seven improved and eight fixed lines (eight is the
most a list keeps).

## After the merge

1. `git tag remote-v21 <merge commit> && git push origin remote-v21` →
the remote-bundles workflow publishes the five bundles and
`manifest.json` as release `remote-v21`.
2. Only then `gh workflow run release.yml --ref main -f version=0.4.1`:
preflight validates version, notes and summaries, CI runs on the exact
commit, and the tag `v0.4.1` with its GitHub release is created.

## Validation

- `bun run check fast` (workflow syntax, generated types, typecheck,
build, unit tests).
- `bun scripts/release-notes.ts 0.4.1` prints the notes (version sources
agree, one heading, summaries in all four languages).
devswha added a commit that referenced this pull request Oct 8, 2026
…e notes (#598)

Two changes the owner asked for, one commit each.

## 1. Clipboard from a pane (OSC 52) is on by default

#570 turned **Settings → Terminal → Clipboard from a pane** off by
default, so copying from vim, tmux and Claude Code silently did nothing.
It is on again; the switch stays for anyone running output they do not
trust.

**Migration: a new storage key.** Settings are saved as one whole
record, so any 0.4.1 install whose user changed *any* setting has
`terminalOsc52: false` stored without choosing it. The choice now lives
under `paneClipboard` (default `true`) and `terminalOsc52` is ignored
and dropped by `sanitizeSettings`, which already discards unknown keys.
That is the simplest correct option:
- a 0.4.1 record loads as on (its `false` cannot be told apart from a
real choice, so it is not trusted);
- an off chosen from now on is written under `paneClipboard` and stays
off across saves and reloads;
- no version field or one-off migration state is needed. A settings
version would do the same with more code.

Known edge: a tab still running the 0.4.1 bundle that saves settings
writes its own record, which has no `paneClipboard`, so a new "off"
would read as on again until it is re-chosen. Tabs pick up the new
bundle on reload, and settings already have no cross-tab sync (each tab
saves its in-memory record), so this is no worse than any other setting.

Tests (`src/lib/settings.test.ts`, "clipboard from a pane"): fresh
default on; a 0.4.1 record with `terminalOsc52: false` loads as on; an
off chosen after the change survives a save, a reload and an unrelated
change. A mutation that falls back to the old key fails the second case.

Also: the setting's description and code comments say what is true now,
ko/ja/zh entries are updated, and there is a CHANGELOG line under
Unreleased → Changed. No doc in `docs/`, `DESIGN.md`, `INSTALL.md` or
`README.md` mentions the default.

## 2. Patch-note style GitHub release notes

`scripts/release-notes.ts` printed the whole CHANGELOG section (181
lines for v0.4.1). It now prints the English lists from
`release-summaries.json` (`### New features`, `### Improvements`, `###
Bug fixes`, a list with no lines left out), then the full section inside
`<details><summary>Full changelog</summary>` with blank lines around it
so GitHub renders the markdown. Lines come through `readSummary`, so
they are trimmed and capped exactly as an install shows them. Every
validation is unchanged (version format, the three version sources,
exactly one heading, nonempty notes, all four languages).
`scripts/release-notes.test.ts` pins the shape, the order and an empty
list left out. `docs/development.md` → Releasing and `scripts/AGENTS.md`
describe the new body.

The published v0.4.1 release is not touched here; the lead regenerates
it after the merge.

### v0.4.1 rendered with this script (`bun scripts/release-notes.ts
0.4.1`)

### New features

- Typing /effort in a Claude Code chat opens a card to pick the effort
for this session
- Settings → Appearance can keep waiting and the latest agents on top of
the Agents list
- A finished agent you opened in the web UI can lose its dot, as it
would in herdr (opt-in)
- The guide shows how to give the app a public HTTPS address with Portal

### Improvements

- A wrong access token waits longer after five tries, up to a minute
- A program in a pane copies to your clipboard only once you allow it in
Settings → Terminal
- Dialogs keep Tab inside them, and screen readers announce tabs, panes
and status lines
- Add PC, Reconnect PC and Update remote bridge open as a bottom sheet
on a phone
- Updating a remote PC that is already current reuses its bridge without
a new download
- Alerts go only to https push services and never follow a redirect
- Long drafts wrap, and the scrollbar no longer covers text when the
page is zoomed

### Bug fixes

- A long line of unclosed brackets or broken escape codes no longer
freezes the chat
- On Windows, more Claude Code and Codex panes show their chat instead
of an error
- Your own devices are recognised by their Tailscale login again,
without pairing
- A Claude Code chat shows what a slash command answered, and /goal is
suggested
- Settings, the palette, Add PC and file previews stack in order; Escape
closes the top one
- The plugin's start keeps the app on its port while herdr is away
- A secret or typed text no longer reaches a pane you have already left
- Gajae Code's selection and startup menus show their choices, not only
arrow keys

<details><summary>Full changelog</summary>

### Added
- `/effort` sent from a Claude Code chat opens a card with the effort
levels, low to max, so the
level no longer has to be set in the terminal. A pick applies to this
session only, as a pick
from the `/model` card does, and leaves the default for new sessions
alone. `/effort` is also in
  the chat's command list now.
  ([#594](#594),
  [#523](#523) by @suho-han)
- Settings → Terminal has a **Clipboard from a pane** switch, off by
default: a program running in a
pane can no longer put text on your clipboard unless you turn it on.
Programs that copy this way
  (vim, tmux, Claude Code) copy again once it is on.
  ([#570](#570) by @radicor)
- The guide's **Behind a reverse proxy** shows how to give the app a
public HTTPS address with
[Portal](https://github.com/gosuda/portal-tunnel) v2.6.1 or later,
behind a long random token
  and with a visitor's `Tailscale-User-Login` header dropped.
([#229](#229) by
@rabbitson87)
- **Settings → Appearance → Agents order → Activity** keeps a waiting
agent on top of the Agents
list and orders the rest by their latest state change, as herdr's agents
panel keeps the latest
work in view: the agent you just sent a message to stays on top while it
runs and after it
finishes, and a new one starts there. herdr's own order and the
workspace rows are unchanged.
  **Workspaces** (herdr's order) remains the default.
([#529](#529) by
@phirschybar)
- **Settings → Appearance → Quiet opened finishes** (off by default): a
finished agent you have
opened in the web UI loses its dot and reads as ready, as viewing it in
herdr would make it.
herdr's DONE otherwise stands until herdr itself shows the pane. It is
remembered per PC in
this browser. ([#529](#529)
by @phirschybar)

### Changed
- A wrong access token is refused with a growing wait after five tries,
up to a minute, whether it
is typed into the sign-in form or sent with a request. Each visitor
behind `tailscale serve` has
their own count, and a browser holding an old token never stops you
signing in with the new one.
  ([#570](#570) by @radicor)
- The app now sends itself a Content-Security-Policy, so third-party
content rendered in a chat —
  math, agent marks — cannot run script in the app.
  ([#570](#570) by @radicor)
- A web-push subscription must be an https endpoint.
  ([#570](#570) by @radicor)

### Fixed
- Gajae Code's selection menus show their choices instead of only
arrow-key buttons, including
startup selectors shown before the pane reports that it is waiting.
Answers move to the selected
  row and recheck the menu before confirming.
  ([#593](#593))
- Secret input and the Codex follow-up fallback validate the live
screen, so a password
prompt or collapsed question queue in scrollback cannot send input into
the current program.
  ([#566](#566))
- The PC's Tailscale login is read from a real Tailscale user id. These
ids are too large for a
JavaScript number, so the owner was not recognised and the owner's own
devices had to pair. Two
  logins with neighbouring ids are also no longer taken for one.
  ([#572](#572))
- On Windows, a Claude Code pane started with a second account's
`~/.claude-*` directory as its
`CLAUDE_CONFIG_DIR` shows its chat. Before, the pane fell back to
`~/.claude`, so the chat said
**Conversation unavailable** and only the terminal worked. Windows does
not let the server read
another process's environment, so the store is the one among the
server's own
`CLAUDE_CONFIG_DIR`, `~/.claude` and the `~/.claude-*` directories
beside it that holds the
Claude process's own record, checked against the time the process
started. A directory
elsewhere is not looked in. Claude Code processes reported as
`claude.exe` are
  recognized too.
([#563](#563) by
@David-Sousa-Web)
- A long line of brackets, `\(` or underscores that never close, as an
agent prints in a log or a
minified file, no longer freezes the chat: a megabyte of them took a
minute or more to read, and
  now takes milliseconds. What every message shows is unchanged.
  ([#574](#574))
- On a mirrored pane (Windows, where herdr cannot attach a terminal),
text typed while a message
was still being sent no longer reaches the pane once you have left it,
or left it and opened it
  again, before the text's turn came.
  ([#576](#576))
- The plugin's `start` keeps the app on its port when the app's own
server holds it but cannot
reach herdr. It used to move the app to another port beside the running
one and blame another
program; now it says that the app runs there without herdr and exits,
and the app answers
  again on its port once herdr is back.
  ([#577](#577))
- On Windows, a Codex pane shows its chat when Codex stored its paths
with the `\\?\` prefix, as
it does for a canonical Windows path (`\\?\D:\work` for `D:\work`).
Before, the chat said
**Conversation unavailable**: the session's file seemed to lie outside
Codex's store, and none of
  the threads matched the pane's directory as herdr reports it.
([#582](#582) by
@David-Sousa-Web)
- In a Claude Code pane, the chat shows what a slash command answered,
so a `/goal` that Claude
Code refuses says why in the chat instead of only in the terminal.
`/goal` is also among the
  commands the message box suggests.
  ([#583](#583))
- Settings opened over a file preview is visible above it; Escape and
Back close Settings
first, preserving the preview and its history entry until the file
itself is closed.
  ([#568](#568))
- Command palette buttons keep their native Enter action; IME commit and
cancel keys
stay with text input, and arrow navigation keeps the selected result
visible.
  ([#567](#567))
- Long drafts in the message box wrap and keep a narrow scroll cue in
reserved space, so the
  scrollbar no longer covers text at fractional zoom.
([#522](#522) by @suho-han)
- A secret sent from a pane you then left and opened again, while
another browser kept the pane
open, is no longer typed into the pane: you send it again from the pane
you opened.
  ([#588](#588))
- A Claude Code slash command whose answer holds a long run of broken
terminal escape codes no
longer stalls the server while the chat reads it. Its answer also drops
a link cut off before
its end and a stray `B` after a charset switch, and an answer with both
output and errors
  shows both.
  ([#588](#588))
- The command palette opened over Settings and a file preview shows
above both, instead of
  taking the keyboard unseen beneath them.
  ([#588](#588))
- The plugin's `start` no longer takes another program on the app's port
for the app because
its answer says `ok`: it treats it as any other program there, moving
the app to a free port,
  or saying the port is taken when `PORT` is set.
  ([#588](#588))
- A 500 from the server no longer repeats the system's own error text,
which carried absolute paths
and the herdr socket location; it names a short id you can quote in a
bug report instead.
  ([#570](#570) by @radicor)
- Settings, the command palette, the file viewer, the file browser and
the new-workspace dialog keep
  Tab inside them and give the focus back to whatever opened them.
  ([#570](#570) by @radicor)
- The tabs of a workspace name the pane region they govern, so a screen
reader announces the tab and
  the pane together.
  ([#570](#570) by @radicor)
- Agent headings in a chat no longer pose as the app's own page
structure; they sit below the app's
  own headings and look the same as before.
  ([#570](#570) by @radicor)
- The "reconnecting" line and the composer's terminal-only hint are
announced when they appear.
  ([#570](#570) by @radicor)
- A chat locked out by the token gate, the "Last checked" line under
Settings → About (with its
date in your language) and a remote PC's state word in the sidebar are
translated like the rest of
  the UI.
  ([#570](#570) by @radicor)
- The alerts menu item now says the same thing the same way in every
state.
  ([#570](#570) by @radicor)
- Held terminal input typed while disconnected is forgotten after a day.
  ([#570](#570) by @radicor)
- A link printed in the terminal opens only if it is an http(s) address,
on both link paths.
  ([#570](#570) by @radicor)
- A second tab open on one pane no longer sends a message the first tab
is already sending: it
sees that send on its way and holds back. Two tabs that press Send at
nearly the same moment can still
  both send it.
  ([#570](#570) by @radicor)
- A row's ⋯ menu is capped to the room its button leaves and scrolls
instead of being cut off by the
viewport, so the pane picker of a tab with many panes keeps every entry
reachable with the pointer
as well as the keyboard. Before, items below the fold were rendered but
unreachable.
  ([#570](#570) by @radicor)
- A row menu open while the window crosses the 640 px breakpoint now
switches between bottom sheet and
  popover instead of keeping the form it opened with.
  ([#570](#570) by @radicor)
- The workspace drawer a narrow window opened is closed again when the
window is widened past 768 px,
  so narrowing it no longer brings back a drawer and its scrim unasked.
  ([#570](#570) by @radicor)
- **Add PC**, **Reconnect PC** and **Update remote bridge** open as a
bottom sheet on a phone, like
every other dialog, and keep clear of the on-screen keyboard. Before,
the one native dialog stayed
  a centred card on a phone.
  ([#570](#570) by @radicor)
- **Remove PC**'s first click is a quiet ghost button that only arms the
removal; the second is the
  red one, as revoking a device already was.
  ([#570](#570) by @radicor)
- A PC's rename, connect and disconnect buttons disable while their
request is in flight, so a double
  click no longer sends two overlapping requests.
  ([#570](#570) by @radicor)
- A failed pane or workspace rename keeps the field open with what you
typed, so a network blip no
  longer makes you write the name again.
  ([#570](#570) by @radicor)
- A workspace reorder that fails no longer undoes a later, successful
reorder.
  ([#570](#570) by @radicor)
- A tab watching several busy panes gives up its oldest cached
conversation answers when they grow
  past a byte budget, not only past sixteen of them.
  ([#570](#570) by @radicor)
- The usage meters' note is the same size as every other advisory and
empty state.
  ([#570](#570) by @radicor)
- The PDF viewer's page colour, the pill radii, the tab dot and the
pairing-code size come from
design tokens now, and the pairing code follows the compact density
setting.
  ([#570](#570) by @radicor)
- Updating an already current remote bridge verifies and reuses it
without downloading or
restarting it again, while a bridge from a newer app is left running
instead of downgraded.
A PC that waits on such a conflict says so in the sidebar and under the
header, with a
  **Reconnect** button, instead of asking for setup approval.
([#519](#519) by @suho-han)
- A wrong access token sent through a proxy on this PC with a made-up
`X-Forwarded-For` address
that itself says " via " now counts against the limit every visitor
through that proxy shares,
like any other wrong token. Before, each such try started a fresh count.
  ([#592](#592))
- A web-push subscription must be an https address, with no exception
for this PC, and an alert is
never sent on where a push service redirects it, so an alert can never
be posted to a service
running on this PC.
([#592](#592))
- In the **Add PC** dialog opened over a file preview, Tab moves through
the dialog's own controls
and Escape closes the dialog alone, leaving the preview beneath it open.
  ([#592](#592))

</details>


## Checks
- `bun run check fast`: ok (1817 pass, 6 platform skips, 0 fail).
- No browser regression covers OSC 52 (`terminal-copy-regression.ts`
covers drag and Ctrl+C copy), so none was run for it.

## Codex review (gpt-6-astra, read-only)
- **Fixed** (P2): summary text was put into the markdown as-is, so
`"…\n\n<!--"` could hide the whole changelog fold. The gate now refuses
a summary line with a line break in any language, and the English lines
escape `<`. A test covers both and fails without the fix.
- **Declined, documented** (P2): a tab still on the 0.4.1 bundle that
saves settings drops `paneClipboard`, so a new "off" reads as on until
it is chosen again. A versioned storage key would stop that, but then
every setting changed in such a tab would be lost instead. It lasts only
until that tab reloads, and every setting added before this one had the
same gap.
- **Pre-existing, not in this diff** (P2): settings have no cross-tab
sync, so a stale tab's next save writes its own copy of every setting,
this one included. Reported to the lead as a follow-up.
devswha added a commit that referenced this pull request Oct 10, 2026
…e stores (#714)

## What was reproduced

No Windows PC here, so every case was reproduced with Win32 path
semantics (`path.win32`) and an in-memory SQLite on Linux:

- **#587 case 1 (prefixed `CODEX_HOME`)**:
`path.win32.relative("\\?\D:\codex\sessions",
"D:\codex\sessions\2026\x.jsonl")` returns the absolute file path, so
the containment check in `codexRolloutPath` rejected a rollout inside
the store whenever only one side kept `\\?\`. The reverse (plain root,
prefixed file) fails the same way.
- **#587 case 2 (case-different cwd)**: `cwd IN ('D:\work\app',
'\\?\D:\work\app')` returns no row for a stored `\\?\d:\work\app`.
- **#586**: with records of the same live process in `~/.claude` and
`~/.claude-copy`, `windowsProcessStore` returned `null`, the same as "no
record", and `claudeTranscriptPath` fell back to `~/.claude`.

## Already fixed on main (#518)

- Point 1 (rollout paths with `\\?\`) and the prefix spelling of point 2
(`cwd IN (?, ?)` with both spellings) were fixed in c04c760 (#582,
released in v0.4.1). Point 3 (picker resume) is #717.

## What changed

- `server/codex.ts`:
- `rolloutInsideStore(root, file, paths)` strips `\\?\` on **both**
sides before `relative()`.
- A Windows cwd is one directory under the `\\?\` prefix and either
drive-letter case, and is otherwise compared exactly.
- `storedCwdCondition(cwd)` gives the three thread queries a binary `cwd
IN (…)` over those spellings, so the queries stay on Codex's `(archived,
cwd, …)` index.
- `sameDirectory` / `codexPeersIn` apply the same rule to the peer
checks (`claimedByOtherPanes`, `resumedElsewhere`, the short-thread
guard), so the peer checks and the queries agree.
  - POSIX cwds are exact as before.
- `server/claude-store.ts`:
- `windowsProcessStore` throws `AmbiguousClaudeStore` when more than one
store holds the process's live PID record. Like a miss, it is not
cached; "no record" still returns `null` and keeps the default-store
fallback.
- Stores are told apart by `claudeStoreKey`: volume + file id where
there is one, else the path without `\\?\` and without case. So
`CLAUDE_CONFIG_DIR=\\?\C:\…\.claude`, a junction or a link to
`~/.claude` is one store, not an ambiguous pair.
- `server/conversation.ts` (one hunk): `AmbiguousClaudeStore` becomes
`ConversationUnavailable("no_session_id")` instead of `~/.claude`.

### Scope narrowed after review

A cwd stored in another letter case **past the drive letter**
(`\\?\d:\Work\app` for `D:\work\app`) is deliberately **not** matched:
the chat stays unavailable.

- A Windows directory can be case-sensitive (`app` and `App` are then
two directories), and only the file system can tell.
- The identity-based version took a stat per spelling, a discovery scan
with a limit and a cache. Two adversarial review passes showed each of
those could fail open: another directory's or another pane's
conversation.
- Fail-closed was chosen instead. #587 therefore stays open for that
case.

## How it was verified

- Unit cases, each failing before its fix:
- `server/codex.test.ts`: a prefixed root or rollout counts as inside
the store, with traversal, another drive and UNC cases.
- `server/codex.test.ts`: the drive-letter and prefix spellings are
found; a case-sensitive sibling with 33 newer threads neither joins nor
crowds out the 32; POSIX case stays exact; the query plan is a SEARCH on
`(archived=? AND cwd=?)`; peers follow the same rule.
- `server/claude-store.test.ts`: two stores that both claim the process
reject with `AmbiguousClaudeStore`; a `CLAUDE_CONFIG_DIR` link to
`~/.claude` is one store; `claudeStoreKey` drops `\\?\` and case without
a file id.
- `bun run typecheck`, `HERDR_TEST_MODE=unit bun test
./server/codex.test.ts ./server/claude-store.test.ts
./server/codex-processes.test.ts ./server/conversation.test.ts
./server/windows-native.test.ts`, `bun run check fast` (all ok).
- Not run on a Windows PC.

## Issues

Closes #586
Refs #587: case 1 and the drive-letter part of case 2 are fixed; a cwd
differing in case past the drive letter stays unavailable by design (see
above).
Refs #518: points 1–2 fixed in #582, point 3 in #717.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
usergood added a commit to usergood/saurons-eye that referenced this pull request Oct 11, 2026
* fix(ci): gate deployment on source-bound verification (#692)

## Change
- Require reusable CI and the site build to succeed for the same SHA
before Pages publishes the website, demo, or installers. Pin checkouts,
keep deployment serialization, and fail closed on stale-main/API errors,
including manual runs.
- Strengthen existing `bun run check`: durable source-bound JSON and
command logs, source/dist invalidation, interrupted/not-run/skipped
states, shared lock for `run`, and `run --build` for browser
prerequisites.
- Retain reports on success and failure. Capture isolated failure
screenshots, bounded console errors, and optional traces without
DOM/network capture in the main UI and prompt-dock regressions.
- Typecheck six critical tooling/demo entrypoints without relaxing
strictness. Document parallel ownership, genuine-dependency-only stacks,
squash/restack validation and explicit authority.

## Validation
- `bun run check fast`: passed after lifecycle fixes; 2242 tests passed,
7 skipped, 0 failed (before the final fail-closed correction, which has
focused coverage below). Includes workflow syntax, generated types, both
typechecks and app build.
- Final focused check/report/browser-evidence/Pages/browser-runner
tests: 39 passed, 233 assertions; both typechecks passed.
- Actual CLI lifecycle smoke: success/failure logs, build provenance,
source/dist mutation invalidation, competing-run lock refusal, SIGTERM
report and subsequent lock release passed in a disposable fixture
repository with a fake herdr executable.
- Actual Playwright synthetic failure: PNG, console metadata, private
artifact modes and trace without DOM/network capture captured from an
isolated fixture.
- Independent policy, deployment/evidence and report reviews completed.
Fixed interrupt cleanup ordering, inherited-output draining,
shared-report overwrite, unavailable source/build identities, and
bounded trace teardown. Actual CLI smoke also covered stubborn
descendants, shared-report lock refusal and source-identity loss.
Required GitHub CI remains authoritative for final SHA f44e9cc.

## Repository settings and remaining credential boundary
- Added `Native Windows install` to required main checks; retained
strict checks, admin enforcement, conversation resolution and optional
approval count. Latest 30 completed CI runs had 27 Windows successes,
one cancellation, and two fork runs awaiting approval without jobs.
- This PR does not provision an agent account or GitHub App credential.
Non-admin implementation credentials require separate operator
provisioning; no claim that current administrator credentials have been
replaced.

## Coordination record
Owner: coordinating maintainer session. Scope: verification/deployment
tooling and operating documentation. Start SHA:
b87d9d50019cdb87422415f7650ad29285a832cc. Branch/worktree:
worktree/rapid-stone-f62b. Dependencies: existing ci.yml/check
entrypoints only; no stacked PR dependency. Integration and release are
explicitly authorized by the user, subject to required checks and
unresolved-review blocking.

* chore(release): prepare 0.4.4 (#694)

## Change
Prepare app release 0.4.4 from main after #692, using the existing
release process only.

- Bump package.json and herdr-plugin.toml together.
- Move the existing Unreleased entries under 0.4.4 (2026-10-09 UTC),
preserving all PR links and external contributor credits.
- Add matching English, Korean, Japanese and Chinese release highlights
(3 new, 1 improved, 4 fixed; every line <=90 characters).
- No tags are created by this PR. The existing Release workflow must
validate the exact main SHA before publishing.

## Validation and review
- `bun scripts/release-notes.ts 0.4.4`: passed, generated expected
release body.
- Release-notes tests: 27 passed, 397 assertions, 0 failures.
- `bun install --frozen-lockfile`: passed without dependency changes.
- Reviewed the release metadata diff; all prior summary entries are
unchanged. The only non-metadata change is the one-line recorder
synchronization fix documented below.
- Audited release-range PR authors and existing changelog credits;
implementation PR #692 is linked.
- Required CI must pass on this PR before squash merge; no administrator
bypass.

Owner: release coordinator (explicitly authorized by user). Start SHA:
ceae7a259fa6f0d16ece3e56abb2178e752541dd. Scope: four release metadata
files and one release-blocking contract-test synchronization line.
Branch: chore/release-0.4.4. Dependencies: merged #692; no open stack.

## Observed release blocker
Website run
[37973801948](https://github.com/devswha/herdr-web-ui/actions/runs/37973801948)
correctly skipped deployment when the existing pending-input contract
test read `barrier` before the recorder observed its final Enter. A
WebSocket receipt is not a receipt from the separate recorder process.

Commit fe09359 adds the existing bounded `waitBytes("barrier\r")` before
the exact-byte and no-enqueue assertions, matching adjacent tests. No
production behavior, expectation, timeout, or skip condition changed.
This tiny prerequisite stays in the release PR rather than creating an
artificial release stack; it is a separate commit for review. No
changelog entry is needed for an internal test synchronization
correction.

Verification on the isolated herdr: full pending-input file **37
passed**, then the affected case **10/10 passed** with `--rerun-each
10`. Source-bound reports were preserved. Required CI is rerunning for
the new PR SHA; no failed check is bypassed or blindly rerun.

* fix(terminal): keep a tab out of use updating; pausing it is a setting (#699)

## Change

**Problem.** Since 0.4.4 (#660), a desktop tab detaches its pane one
second after the user switches to another window, so that herdr's own
TUI gets the pane's size back. The tab then stops updating until it is
clicked again. To someone who does not run herdr's TUI next to the
browser, this looks like the terminal froze. It also happens on a second
monitor where the tab stays in view.

**Change.**
- A tab that is out of use keeps its pane and its output again, as in
0.4.3. It still sends no resize and reconnects with `keep_size`. Only
the release is affected.
- **Settings → Terminal → Pause in another window** (`releasePaneAway`,
off by default) turns #660's release back on, unchanged, for people who
also work in herdr's own TUI on the same PC.
- The herdr API has no read-only way to tell whether a TUI client shows
the pane: the snapshot has no client field, and `no_foreground_client`
comes only from `client.window_title.set/clear`, which change the user's
title. So the choice stays with the user.

@jiunshinn: #660 made a desktop tab stop updating a second after the
user switches windows, and we had a report that this reads as a frozen
terminal. This PR makes that release opt-in, off by default. A follow-up
will make the setting switch the tab to a read-only `herdr terminal
session observe` stream instead of pausing. I measured on 0.9.3 that the
observer leaves herdr's TUI at its own size (44x37 stays 44x37, while an
attach makes it 150x50), so the tab keeps updating and the TUI is not
cut off. Feedback welcome.

## Validation

- `scripts/chat-size-regression.ts` against a `bun run check run
--build` herdr: 20/20 PASS. The new check (default settings, window
blurred, clock run past the 1 s release delay) confirms there is no
`detach`, no paused banner, and the pane's `pty-data` still arrives. The
existing #660 checks run with the setting on and still pass.
- `bun run check fast`: workflow syntax, generated types, typecheck,
build, unit tests all ok.
- `src/lib/settings.test.ts`: the setting is off by default and only a
boolean `true` turns it on.
- Settings → Terminal opened in a browser against the isolated test
session: the toggle shows up off, turns on, and is saved to
`herdr-web-ui:settings`. Korean, Japanese and Chinese strings were
added, and `i18n.test.ts` passes.

* feat(terminal): a tab out of use watches its pane read-only instead of pausing (#700)

## Change

**Problem.** #699 made #660's release opt-in: with the setting on, a
desktop tab out of use detaches its pane so that herdr's own TUI gets
the pane's size back, and then the tab freezes until it is clicked.
People who run herdr's TUI next to the browser had to pick one of two
problems: a frozen web terminal, or a TUI cut off at the browser's size.

**Fix.** herdr ≥ 0.7.2 has a read-only observer, `herdr terminal session
observe <pane> --cols --rows`. Unlike `terminal attach`, it does not
take ownership of the pane's size. I measured this on 0.9.3 with a real
TUI client and a split pane at 44 columns:

| state | pane size (`stty size`) |
|---|---|
| TUI only | 37 44 |
| browser tab in use (attach) | 37 145 |
| browser tab out of use, **watching** | **37 44** |
| browser back in use | 37 145 |

With the setting on, the tab now detaches as before, then watches the
pane instead of pausing. The terminal shows "View only while you use
another window · click to type". Focus or a pointerdown unwatches and
attaches again at the tab's size. A grid change starts the view again at
the new grid. A released tab that moves to another pane watches that
one.

- **Wire** (`shared/protocol.ts`, feature `"watch"`): client `watch
{pane_id, cols, rows}` / `unwatch {pane_id}`; server `watch-data
{pane_id, data}` / `watch-end {pane_id}`. A watch is not an attach:
there is no input, resize, ACK or input-ready, and it never touches
`attachments`, claims or `pane-geometry`, so a phone can keep its attach
while a desktop watches.
- **Server** (`server/watch.ts`): one observer per (connection, pane),
started with plain `Bun.spawn`. It is not the PTY sidecar.
`HERDR_SOCKET_PATH` is pinned the same way the attach does it. Frames
are NDJSON; `bytes` is base64 of ANSI, and herdr labels it
`encoding:"ansi"`. They are decoded with a streaming UTF-8 decoder. The
observer is killed on unwatch, a replacing watch, WS close, a
slow-client stop, device revoke and `stop()`.
- **Fallbacks:** an older bridge has no `"watch"` feature, and a
`watch-end` can arrive when the pane is gone or herdr has no observer.
In both cases the tab stays paused on its last screen, as before. The
site's demo answers `watch` with `watch-end`; its page never releases
anyway.
- **Setting renamed** to describe the situation rather than the
mechanism: **Use alongside herdr's own window** (still off by default,
same `releasePaneAway` key, not yet released).
- The herdr API has no read-only way to detect a TUI client, so this
stays a setting. `herdr api schema --json` has no client listing, and
`no_foreground_client` only comes from `client.window_title.set/clear`,
which change the user's title.

## Validation

- `server/watch.contract.test.ts` (new, live herdr via `bun run check
run`), 6/6 passing: the `"watch"` feature is advertised; an unattached
pane streams real command output to an observe-role client while no
`terminal attach` process runs; unwatch kills the observer without a
`watch-end`; WS close kills it; a missing pane gets `watch-end`; invalid
geometry gets `invalid_geometry`.
- `server/watch.test.ts` (new, unit): NDJSON across chunk boundaries, a
UTF-8 character split across two frames, `terminal.closed`, unknown
records.
- `src/lib/ws.test.ts`: a watch goes out after the snapshot and again
after a reconnect; it is not re-sent after `watch-end`; a reconnect to a
bridge without the feature ends it.
- `scripts/chat-size-regression.ts` against `bun run check run --build`,
21/21 PASS. The release checks now expect the view-only banner and a
`watch` frame. New: once the phone leaves and no attach runs, the window
still receives the pane's output through `watch-data`.
- End to end, by hand, with a real herdr TUI in tmux on a throwaway
session: the sizes in the table above, and `echo LIVE-WHILE-WATCHING`
showed up in the browser while it watched.
- `bun run check fast`: all ok. New strings are in ko/ja/zh.

* fix(claude): certify background counts against scanned file versions (#701)

## Change
Fix R1 from the independent v0.4.4 review: a concurrent parent append
could be certified under the newer post-scan signature, freezing a stale
count. Cache only identical pre/post scan versions and dependencies that
were quiet before the scan. Parent identity and folder replacement now
participate in invalidation.

## Validation
Reproduced the race 3/3 before the fix; all 3 after-fix probes converge.
Focused public behavior regressions cover replacement and stable reads.
The complete remediation rebased on current main passes `bun run check
full`: 2,259 unit passes (7 platform skips), 323 integration passes, all
20 browser scripts and the demo build. This PR also requires its own CI
before merge.

## Ownership
owner: devswha via Amp
scope: R1 count-cache certification only
branch/worktree: fix/claude-count-scan / dedicated
release-review-remediation worktree
start SHA: 3a5b05068a5a2c623cb9dacd372e351a61ad2470
dependencies: none
verification: local combined full check passed; PR CI pending
status: ready for review

No code adapted from another project.

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(claude): retry incomplete process identity at bounded intervals (#702)

## Change
Fix verified R3 from the v0.4.4 review: an initial missing PID or
process-start lookup used to stay unknown forever. Retry incomplete
identity at the existing bounded interval, including resolver-only
callers. A recovered process boundary correctly marks tasks from the
previous process lost; no timestamp is invented.

## Validation
Reproduced the sticky unknown identity before fixing. Public status
tests cover a missing PID, missing start and resolver-only recovery.
Focused claude-subagents tests pass on current main. The combined
remediation full run on the latest terminal changes and metadata
identity fix passed 2,259 unit tests, 323 integration tests and all 20
browser scripts plus demo build. This PR requires its own CI and native
gates.

## Ownership
owner: devswha via Amp
scope: R3 process identity recovery
branch/worktree: fix/claude-process-identity / dedicated remediation
worktree
start SHA: 36209a0 (main after #701)
dependencies: none; based on latest main
verification: focused tests passed; PR CI pending
status: ready for review

Changelog PR links will be finalized in the release preparation PR. No
externally adapted code.

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(claude): retain observed rest across delayed transcript discovery (#703)

## Change
Fix verified R2: delayed transcript discovery used to erase the observed
working-to-rest transition, leaving a turn with background work at DONE
instead of BG. Associate independently observed rest times with the
discovered prompt; keep the first eligible deadline across automatic
resumes. A positively replaced session/process clears the old wait
evidence, including equal-count transitions. Initial
discovery/refinement does not reset it.

This does not suppress or retract an initial DONE notification emitted
before discovery; that remains a separate product policy.

## Validation
Reproduced early vs delayed discovery before fixing. State/concurrency
design was checked with Oracle. Tests cover delayed discovery,
first-deadline preservation, initially resting panes and positive
session replacement. Existing live contract tests check exact frames and
one final push; their injected clock and transcript timestamps now share
one time domain. All 9 contract tests and the complete combined full
check passed; focused unit checks pass again on latest main. This PR
must pass its own CI/native/remote gates.

## Ownership
owner: devswha via Amp
scope: R2 delayed BG state
branch/worktree: fix/claude-delayed-background / dedicated remediation
worktree
start SHA: d4fa866 (main after #702)
dependencies: integrated process-identity recovery
verification: local focused and combined full checks passed; PR CI
pending
status: ready for review

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(licenses): include the bundled TanStack Highlight license (#704)

## Change
Fix verified R4 from the v0.4.4 independent review: the distributed
notices omitted the bundled TanStack Highlight MIT text. Include the
exact installed upstream license and a focused regression comparing it
with the dependency license. No runtime behavior changes.

## Validation
The license test passes on latest main. Production and demo builds were
checked byte-for-byte against THIRD_PARTY_NOTICES.md. The combined
remediation with review corrections and the readline fixture input-order
fix passes the full local suite: 2,260 unit tests, 323 integration
tests, all 20 browser scripts plus demo build. This PR also requires its
own CI.

## Ownership
owner: devswha via Amp
scope: bundled Highlight license only
branch/worktree: fix/highlight-license / dedicated remediation worktree
start SHA: c824aab (main after #703)
dependencies: none
verification: focused license test and combined full suite passed; PR CI
pending
status: ready for review

The added text is the MIT license distributed by @tanstack/highlight;
implementation code is not adapted.

Co-authored-by: Amp <amp@ampcode.com>

* fix(deps): update source-map-js to patched 1.2.2 (#705)

## Change
Resolve the inherited high-severity source-map-js advisory identified in
the v0.4.4 release review. This is a lockfile-only 1.2.1 → 1.2.2 patch;
no dependency range or unrelated package changes.

## Validation
The original audit reported one high advisory. After the patch, frozen
install succeeds and bun audit reports zero vulnerabilities across 159
packages. The combined remediation full suite and production/demo builds
pass. Native CI and remote bundle/SSH validation must pass on this PR
before merge.

## Ownership
owner: devswha via Amp
scope: source-map-js patch only
branch/worktree: fix/source-map-security / dedicated remediation
worktree
start SHA: 814754d (main after #704)
dependencies: none
verification: frozen install, audit and combined full suite passed; PR
CI pending
status: ready for review

Co-authored-by: Amp <amp@ampcode.com>

* fix(claude): invalidate background counts at task age boundaries (#706)

## Change
Fix verified R6: an unchanged file signature bypassed the existing
one-day task age rule, so a warm badge kept a task that a direct read
had already expired. Include the age eligibility boundary in the watched
signature using the same clock as the poll. No new age policy or
configurable expiry is introduced.

## Validation
Before/after probes reproduced a warm count of 1 vs direct count 0;
after the fix both are 0. Public status regression covers both sides of
the age boundary without any file write. Focused claude-subagents tests
pass on latest main; the combined full suite with review corrections
passes 2,260 unit tests, 323 integration tests and 20 browser scripts
plus demo build. PR CI must pass separately.

## Ownership
owner: devswha via Amp
scope: R6 age-boundary invalidation
branch/worktree: fix/claude-task-age / dedicated remediation worktree
start SHA: 56d9ae0 (main after #705)
dependencies: integrated count-cache certification
verification: focused and combined full checks passed; PR CI pending
status: ready for review

---------

Co-authored-by: Amp <amp@ampcode.com>

* test(ci): gate releases on native session identity regressions (#709)

## Changes
Run existing Windows-native identity tests in reusable CI and add native
macOS Claude-store/process-start coverage. Release and Website callers
inherit the checks. Preserve existing platform coverage and all
assertions.

Also fix the verified readline fixture input race: prior browser
keystrokes could prefix the direct-RPC launch command. Send cleanup and
launch through the same browser input stream. Separate commits preserve
the independent changes.

## Validation and CI timing
Typecheck passed on this branch. Combined full local validation passed
(2260 unit, 323 integration, 20 browser scripts). Native behavior must
pass on the corresponding GitHub runners before merge.
Measured integration job queue 2s versus execution 485s; browser lane
462.9s and integration lane 208.7s already overlap. No test deletion,
relaxed deadline, new cache or gate reuse. Structural sharding remains
follow-up work.

## Ownership
- owner: devswha / Amp
- scope: native validation registration and verified browser fixture
race
- branch/worktree: test/native-release-validation / dedicated
release-review-remediation
- start SHA: d12a77c
- dependencies: none
- verification: local typecheck, full combined run, native and browser
PR CI
- status: ready for review and CI

Amp-Thread-ID:
https://ampcode.com/threads/T-01a12235-af16-73b1-ad0c-625b633f2697

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(chat): retain page-scoped Claude completion identities (#708)

## Change
Keep completion identities across settled/live transcript boundaries
within the requested page. Reparse live suffixes with a copy, so later
polls and resumed completions remain visible. Do not deduplicate across
pages.

## Validation
Regression reproduced before the fix. Restacked conversation suite: 78
pass. Combined remediation check full passed; required PR checks run on
this exact revision.

## Ownership
- owner: devswha / Amp
- scope: Claude completion identity cache and regression
- branch/worktree: fix/claude-completion-dedup in dedicated
release-review-remediation worktree
- start SHA: d12a77c (latest origin/main)
- dependencies: none
- verification: focused unit suite, required CI
- status: ready for independent review and CI

Amp-Thread-ID:
https://ampcode.com/threads/T-01a12235-af16-73b1-ad0c-625b633f2697

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(chat): preserve trailing code whitespace in manual selections (#710)

## Change
Normalize CRLF only; preserve trailing source newlines in rendered code
for manual selection with highlighting on or off. Copy code still uses
the original source.

## Validation
Focused unit tests passed on this branch. The combined full check
passed, including file-viewer DOM/Range assertions for both highlighting
settings. Synthetic rendered screenshots were inspected:
- On:
https://ampcode.com/user-content/artifacts/d1a195977397e5703819cda7bdd33f742c8d4cf6dd62c538e9a6ccb498a01be0-file.png
- Off:
https://ampcode.com/user-content/artifacts/c3bca89c20ccadc70690e0984208a6274b8314387aec34c2b189d801e45060bc-file.png
Required CI runs again for this PR.

## Ownership
- owner: devswha / Amp
- scope: source whitespace fidelity and regression coverage
- branch/worktree: fix/code-selection-whitespace / dedicated
release-review-remediation
- start SHA: d12a77c
- dependencies: none
- verification: unit tests, browser DOM/Range and visual inspection, PR
CI
- status: ready for review and CI

Amp-Thread-ID:
https://ampcode.com/threads/T-01a12235-af16-73b1-ad0c-625b633f2697

---------

Co-authored-by: Amp <amp@ampcode.com>

* chore(release): prepare v0.4.5 and remote runtime v24 (#711)

## Release
Prepare patch v0.4.5 with matching package/plugin versions, dated
changelog, four-language update summaries and remote bundle version 24.

Includes independent review remediation #701–#706, #708–#710, plus
already merged #699/#700. All product PRs passed required CI on their
current base; native Windows and macOS validation is now inherited by
Release. All review threads are resolved.

Remote-v24 MUST publish all five checked archives and manifest before
dispatching Release for v0.4.5. Remote PCs will require Update bridge…
once. Never push the app v-tag manually.

## Validation
Metadata generation and frozen install/audit passed. A fresh complete
local check and required PR CI run on this integrated revision. Previous
combined check: 2260 unit, 323 integration, 20 browser scripts plus demo
build. Actual native jobs: 11 Windows and 37 macOS tests passed.

## CI investigation
Queue versus execution measured separately: integration/browser 2s/485s
before, 13s/493s after fixture correction. No speedup claimed. Official
ECR Debian mirror avoids verified Docker Hub pull throttling; browser
fixture now orders cleanup and launch with earlier input. Tests, support
platforms and exact-SHA release/deployment gates are retained. Browser
sharding or validation reuse are separate follow-up proposals.

## Limitations
Initial alerts emitted before transcript discovery cannot be retracted;
status events lack process identity. Completion dedup is page-scoped.
Real phone/Safari/PWA naming, live paid agents and delivered production
push timing remain unverified. Optional architecture cleanup is
excluded.

## Ownership
- owner: devswha / Amp
- scope: v0.4.5 release metadata and remote-v24
- branch/worktree: chore/release-v0.4.5 / dedicated
release-review-remediation
- start SHA: e6a7f81
- dependencies: all product remediation PRs merged
- verification: fresh check full, native CI, five platform bundle
checks, Release exact-SHA gate
- status: release candidate; publish only after gates pass

Amp-Thread-ID:
https://ampcode.com/threads/T-01a12235-af16-73b1-ad0c-625b633f2697

---------

Co-authored-by: Amp <amp@ampcode.com>

* docs(agents): drop the Session panel row, whose component is gone (#693)

The `Session panel` row in the WHERE TO LOOK table named four things:

- `components/SessionPanel.tsx`
- `lib/activity.ts`
- an `onActivity` prop on ChatView
- a `herdr-web-ui:session-panel` storage key

None of them exist. `grep -rn SessionPanel src/` matches this row and
nothing else; `onActivity` and `session-panel` likewise appear nowhere
but here.

`components/BackgroundTasks.tsx` is **not** a rename of that surface —
it is omo task runs polled from the server (`lib/omoTasks.ts`,
`OmoTask`), where the row described turns derived client-side inside
ChatView. Repointing the row at it would have been a plausible-looking
lie, so the row is deleted.

No behaviour change. Found by the V8 finding of the 2026-10-06
accessibility/visual audit.

Co-authored-by: Hako <devswha@gmail.com>

* ci: typecheck three more entry-point scripts (#696)

No code changes. `tsconfig.tools.json` covers six entry points; three
more that CI actually runs typecheck clean under it today and are simply
not listed:

- `scripts/build-remote-bundle.ts` (`build:remote`)
- `scripts/ssh-integration.ts` (`test:ssh`)
- `scripts/file-viewer-regression.ts` (the browser lane — AGENTS.md
notes `test:ui` skips it but CI runs it)

Adding them makes `bun run typecheck` fail on a missing or mistyped
import in any of the three, which is the failure class that let a bad
commit reach `main` green before.

## What is still uncovered, and why it is not in this PR

The one I most wanted is `scripts/ui-regression.ts` — the main browser
entry point. Its transitive closure carries 17 errors across five files:

```
7  scripts/notification-startup-regression.ts
4  scripts/mobile-viewport-regression.ts
3  scripts/alert-sound-regression.ts
2  scripts/ui-regression.ts
1  scripts/csp-violations.ts
```

Two of those are worth your eye regardless of the typecheck question:

- `ui-regression.ts:304` uses `Array.prototype.findLast`, which needs
`lib: ES2023`; the root tsconfig is `target: ES2022`. That is a config
gap rather than a script bug, and bumping `lib` is a project-wide call I
did not want to make inside a hygiene PR.
- `csp-violations.ts:72` — `Object is possibly undefined`.

`generate-protocol-types.ts` is also clean-except-for-one (an unused
local at :114), so I left it out rather than bundle a code change into a
PR promising none.

Fixing the 17 is a small, mechanical follow-up if you want the browser
lane covered — say so and I will do it as its own PR, or fold the `lib`
bump in separately if you would rather have the coverage than the
smaller diff now.

Co-authored-by: Hako <devswha@gmail.com>

* fix(a11y): never read a pane id out as the terminal's name (#695)

Follow-up to #633. That PR labels the terminal region with the pane's
title and falls back to the pane id:

```ts
paneId === null ? t("Terminal") : t("Terminal for {title}", { title: title ?? paneId })
```

`title` is null until the snapshot that names the pane arrives, so a
pane opened on that gap announces itself as **"Terminal for
p_1a2b3c4d"** — a screen reader reads the id aloud as the name of the
product's central surface.

A pane with nothing to call it now takes the grid's own kind, which is
the name the idle grid (`paneId === null`) already uses.

The rule is pulled into `src/lib/terminalLabel.ts` so it can be pinned
by a test — this is the one a11y surface in the app with no unit
coverage at all, and the failure mode (a fallback string) is exactly
what a test should hold.

```
bun test ./src/lib/terminalLabel.test.ts   2 pass
bun run typecheck                          clean
bun run test:unit                          1720 pass / 0 fail
```

No new translation keys: both strings already ship for #633.

**Scope note, not fixed here.** `displayPaneTitle()`
(src/components/Sidebar.tsx:55) itself falls back to `pane.pane_id` when
a pane has no label and no derivable title, so a genuinely untitled pane
can still carry an id here — and it shows one in the sidebar today as
well. Distinguishing "no title yet" from "no title to be had" needs a
nullable accessor beside `displayPaneTitle`, which changes that
function's contract and is a wider change than this. Happy to do it as a
follow-up if you want it; say the word.

---------

Co-authored-by: Hako <devswha@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(security): a session cookie with no Origin is cross-site (#697)

The S6 finding of the 2026-10-06 audit. `sameOrigin()` returns `true`
when the `Origin` header is absent, so a cookie-bearing request whose
`Origin` went missing — or was stripped by something in front — passes
the origin gate.

A browser always attaches `Origin` to a cross-origin state-changing
request, and the WS upgrade already carries the device cookie, so this
is not a live browser hole: `SameSite=Strict` covers the case. It is
defence in depth, and the audit rated it Low for that reason.

**What I am not claiming:** I did not find an exploit chain, and I am
not asking you to treat this as one.

## The three paths that make this hard

This is the second attempt. The first was reverted in `e755194` because
tightening `sameOrigin` broke the documented wire contract. These three
carry a session cookie and state no `Origin` in exactly the cases that
need them:

| Path | Why it cannot state one |
|---|---|
| `GET /ws` upgrade | a browser cannot attach the custom mutation header
to a WebSocket handshake |
| `DELETE /api/auth` | sign-out; it manages the sender's own session |
| `POST /api/push/subscribe` | the same session's own push subscription
|

So each is exempted by path, and each is now pinned:

- `server/auth.test.ts` — 11 cases: the three exempted, the two cookies
alone, the `x-herdr-machine` proof, a CLI with no cookie, and a browser
that does state an origin.
- `server/open-access.test.ts` — the same thing through the HTTP seam: a
watch device with no `Origin` is refused `invalid_origin`, and with the
header it gets past the origin gate and is refused `read_only` instead —
i.e. the origin check stopped being the thing answering.

```
bun test ./server/auth.test.ts ./server/open-access.test.ts    21 pass / 0 fail
```

I also ran a throwaway probe of those 11 cases against `main` without
this change: 5 of them flip to `true`, so the tests are not passing
vacuously.

## Notes

- `machine-security.ts` is byte-identical to the copy this was written
against, so the change carried over without a conflict.
- `pty-compare.ts` was in the same working tree and is deliberately not
included — unrelated.
- If you would rather this sat as a recorded observation than a change
(the audit already lists S6 as `"recorded"`), close it and I will drop
it. I would rather ask than assume.

---------

Co-authored-by: Hako <devswha@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(machines): show an unreachable PC as reconnecting after a failed bridge update (#716)

## Problem

A registered remote PC that is switched off kept showing **Bridge update
needed** / "needs a bridge update to reconnect" with an **Update
bridge** button that cannot succeed.

## Root cause

`MachineManager.setup()`'s catch put every failed bridge update of a
registered PC (other than a `MachineActionRequired`) into `state:
"error"`, `action_required: "update_bridge"`, including a failure that
came from SSH not reaching the PC at all. Nothing re-probes an `error`
state, so the prompt stayed until the user acted.

The likely path: after an app update raises `REMOTE_BUNDLE_VERSION`, the
reconnect finds the old bridge and queues the automatic update. That
update opens a new SSH connection. If the MacBook has gone to sleep or
been switched off by then (or drops out mid-update), the connect fails
and the PC is parked on the update prompt for good.

## Fix

- An update job that could not reach a registered PC no longer parks it.
It goes back on the normal reconnect backoff (`retryLater`, factored out
of `lost()`), shows **Reconnecting…** with ssh's error, and has
`action_required: null`. When the PC answers, `reconnect()` re-runs the
version check, so `update_bridge` (and the automatic update) comes back
only when the bridge really is out of date.
- "Could not reach" means one of two things. SSH never came up and ssh's
error is not a refusal (`Permission denied`, host-key failures). Or SSH
came up and the master connection then died (new
`SshConnection.connected()`, read before `disconnect` closes it).
- Behaviour that stays the same: a PC that refuses the saved key keeps
**Update bridge** / **Sign in and update…**, as the `updateBridge`
comment requires. An update that fails on a PC that still answers
(download, install) also keeps the button and its reason. The job's own
`action_required`, which the dialog reads, is unchanged.
- No new UI strings. CHANGELOG (Unreleased → Fixed) and
`docs/remote-pcs.md` are updated.

## Tests

`server/machines.test.ts` has four new cases on a registered PC with SSH
faked:
- connect timeout → `reconnecting`, no action
- connection dropped mid-update → `reconnecting`, no action
- `Permission denied (publickey)` → keeps `update_bridge`
- a failure on a PC that still answers → keeps `update_bridge`

The first two fail without the fix. `bun run check fast` passes.

## Second path: a cancelled update (182bdd8)

On the user's live server, the MacBook record read `state:
"disconnected"`, `action_required: "update_bridge"`, with the error
`mux_client_request_session: read from master failed: Broken pipe …
Connection timed out`. That record has two steps behind it:
1. An earlier update failed after the SSH master died. The old catch
stored that error, `update_bridge` and `state: "error"`. The first
commit now turns this into reconnecting.
2. A later update job was **cancelled** (Cancel update, a closed dialog,
or the 600 s `job.timer`). The catch calls `disconnect()`, which sets
`"disconnected"`, then skips everything else for a cancelled phase. The
stale `action_required`/`error` stay, and no retry is scheduled, so the
PC stays on the prompt for good.

The new test `reconnects after a cancelled update, and asks again only
once the PC answers` reproduces that exact record without the fix.

The fix: a cancelled update of a registered, enabled PC (owning the
runtime, server not stopping) clears both fields and goes back on the
reconnect backoff. When the PC answers with an old bridge, it shows the
**Update bridge** button. The cancelled automatic update does not
restart by itself (`updateCancelled`) until the PC connects or the user
starts an update.

**Restarts and long outages:** `machines.json` stores no state, error or
action, so a restart starts every PC `disconnected` and reconnects it.
While a PC stays off, `reconnect()` fails at `ssh.start` and `lost()`
keeps it reconnecting. `action_required` is set in only two places: when
`prepare`/`verify` throw `MachineActionRequired` (SSH up and the PC
answering), or in the setup catch for a refusal or a failure with the
master still alive (the PC answered).

## Not covered / not verified

- Not tried against a real Mac that goes offline; the SSH failures are
faked at `SshConnection`.
- A PC whose update failed while it was still reachable, and which was
turned off afterwards, still shows the update prompt, because nothing
re-probes an `error` state. A periodic SSH liveness probe could cover
that case. It would change how the prompt behaves, so I left it for a
separate decision.
- Telling a refusal apart from an unreachable PC before SSH comes up
relies on ssh's stderr wording (OpenSSH's standard messages).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(omo): read no DONE for a turn that ended on a model error (#715)

## What was reproduced

An OmO pane's status comes from its session file
(`server/omo-status.ts`). An assistant answer with `stopReason: "error"`
— what OmO writes when the provider gives up after its retries or a
request times out — ended the turn exactly like a normal `stop`.
`CompletionTracker.settle()` then saw "idle after work" and latched the
pane as `done`, so it read DONE and web push / the browser alerted "work
finished" while the agent was still alive.

The new test in `server/omo-status.test.ts` replays the issue's sequence
as OmO records it: work, an error answer ("…could not complete the
request after 5 retries…"), the stop-state record, a `model_change`
fallback, more work on the fallback model, a second error ("Request
timed out."). Before the fix it read `working, done, working, done`.

## What changed

- `omoTurnAfter` marks a rest that came from an `error` answer as
`failed`. The pane still reads READY (not RUN: nothing OmO writes tells
a retry from giving up, as before).
- `OmoStatus.poll` tells that rest as `rest: "failed"` on `onChange`;
`omoChanged` passes it to `CompletionTracker.observe(…, rest)`, which
drops the pane's "worked" mark and any DONE kept from an earlier turn:
READY, no DONE, no alert. Snapshots, resync and restarts then agree,
since nothing is latched.
- A later answer after a failed rest (a retry that got through, or a new
prompt answered within one poll) is told as `rest: "answered"`: the
tracker marks the work itself and the pane goes straight to DONE. No
synthetic RUN is sent first, since that would make the turn 0 ms long
and the default "long turn" alerts would skip it. With no turn start
seen, push and the browser treat the length as unknown and alert.
- The failed rest is kept with what the pane read when its session could
not be told for a while, so a retry that answered meanwhile is still a
finish.
- Kept conservative (a missed DONE rather than a made-up or repeated
one): the remembered failure belongs to its session file; only an answer
timestamped after the failure counts as its retry; the tracker accepts
it only while nothing (work, DONE) was told of the pane since the failed
rest; an error read from a file found anew (restart, `/resume`) is
`failed-before` and leaves a DONE kept from another turn alone.
- Known limit: a retry's DONE that never read RUN has an unknown length
to both alert paths, so the default "long turn" setting alerts it even
after a short retry. Carrying the real duration needs turn timing in the
pane-status frames; left for a follow-up.
- Other agents are untouched: the flag is only set by OmO's session
reader and `observe` defaults to `failed = false`.

Not changed: `stopReason: "aborted"` (user interrupt) still ends as
DONE; an OmO pane whose session file cannot be told keeps herdr's status
as before. Wrong DONEs already saved in `completions.json` clear on the
pane's next turn or focus.

## How it was verified

- New unit tests: the issue's sequence reads `working, idle, working,
idle`, READY in every snapshot after; a retry's answer then reads
`done`; a normal turn afterwards still reads `working, done`. A DONE
kept from an earlier turn is dropped by a failed rest, and a retry that
answered while the session could not be told is still DONE.
- `bun run typecheck`, `HERDR_TEST_MODE=unit bun test` for `completion`,
`omo-status`, `omo`, `omo-tasks`.
- `bun run check run bun test ./server/omo.contract.test.ts
./server/omo-prompt.contract.test.ts` on an isolated herdr: 10 pass.
- `bun run check fast`: all ok (2261 pass, 0 fail).
- Alerts: web push (`server/push.ts`) and the browser's alerts
(`shouldNotifyStatus` in `shared/notify-policy.ts`) fire only on
`done`/`blocked`, so a READY rest alerts nothing on either path.

Closes #687

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(chat): match Windows Codex store paths and refuse ambiguous Claude stores (#714)

## What was reproduced

No Windows PC here, so every case was reproduced with Win32 path
semantics (`path.win32`) and an in-memory SQLite on Linux:

- **#587 case 1 (prefixed `CODEX_HOME`)**:
`path.win32.relative("\\?\D:\codex\sessions",
"D:\codex\sessions\2026\x.jsonl")` returns the absolute file path, so
the containment check in `codexRolloutPath` rejected a rollout inside
the store whenever only one side kept `\\?\`. The reverse (plain root,
prefixed file) fails the same way.
- **#587 case 2 (case-different cwd)**: `cwd IN ('D:\work\app',
'\\?\D:\work\app')` returns no row for a stored `\\?\d:\work\app`.
- **#586**: with records of the same live process in `~/.claude` and
`~/.claude-copy`, `windowsProcessStore` returned `null`, the same as "no
record", and `claudeTranscriptPath` fell back to `~/.claude`.

## Already fixed on main (#518)

- Point 1 (rollout paths with `\\?\`) and the prefix spelling of point 2
(`cwd IN (?, ?)` with both spellings) were fixed in c04c760 (#582,
released in v0.4.1). Point 3 (picker resume) is #717.

## What changed

- `server/codex.ts`:
- `rolloutInsideStore(root, file, paths)` strips `\\?\` on **both**
sides before `relative()`.
- A Windows cwd is one directory under the `\\?\` prefix and either
drive-letter case, and is otherwise compared exactly.
- `storedCwdCondition(cwd)` gives the three thread queries a binary `cwd
IN (…)` over those spellings, so the queries stay on Codex's `(archived,
cwd, …)` index.
- `sameDirectory` / `codexPeersIn` apply the same rule to the peer
checks (`claimedByOtherPanes`, `resumedElsewhere`, the short-thread
guard), so the peer checks and the queries agree.
  - POSIX cwds are exact as before.
- `server/claude-store.ts`:
- `windowsProcessStore` throws `AmbiguousClaudeStore` when more than one
store holds the process's live PID record. Like a miss, it is not
cached; "no record" still returns `null` and keeps the default-store
fallback.
- Stores are told apart by `claudeStoreKey`: volume + file id where
there is one, else the path without `\\?\` and without case. So
`CLAUDE_CONFIG_DIR=\\?\C:\…\.claude`, a junction or a link to
`~/.claude` is one store, not an ambiguous pair.
- `server/conversation.ts` (one hunk): `AmbiguousClaudeStore` becomes
`ConversationUnavailable("no_session_id")` instead of `~/.claude`.

### Scope narrowed after review

A cwd stored in another letter case **past the drive letter**
(`\\?\d:\Work\app` for `D:\work\app`) is deliberately **not** matched:
the chat stays unavailable.

- A Windows directory can be case-sensitive (`app` and `App` are then
two directories), and only the file system can tell.
- The identity-based version took a stat per spelling, a discovery scan
with a limit and a cache. Two adversarial review passes showed each of
those could fail open: another directory's or another pane's
conversation.
- Fail-closed was chosen instead. #587 therefore stays open for that
case.

## How it was verified

- Unit cases, each failing before its fix:
- `server/codex.test.ts`: a prefixed root or rollout counts as inside
the store, with traversal, another drive and UNC cases.
- `server/codex.test.ts`: the drive-letter and prefix spellings are
found; a case-sensitive sibling with 33 newer threads neither joins nor
crowds out the 32; POSIX case stays exact; the query plan is a SEARCH on
`(archived=? AND cwd=?)`; peers follow the same rule.
- `server/claude-store.test.ts`: two stores that both claim the process
reject with `AmbiguousClaudeStore`; a `CLAUDE_CONFIG_DIR` link to
`~/.claude` is one store; `claudeStoreKey` drops `\\?\` and case without
a file id.
- `bun run typecheck`, `HERDR_TEST_MODE=unit bun test
./server/codex.test.ts ./server/claude-store.test.ts
./server/codex-processes.test.ts ./server/conversation.test.ts
./server/windows-native.test.ts`, `bun run check fast` (all ok).
- Not run on a Windows PC.

## Issues

Closes #586
Refs #587: case 1 and the drive-letter part of case 2 are fixed; a cwd
differing in case past the drive letter stays unavailable by design (see
above).
Refs #518: points 1–2 fixed in #582, point 3 in #717.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(server): use pane.get for single-pane lookups (#724)

## Change

Fix audit finding H6 by replacing whole-session snapshots used only to
inspect one pane.

- Add a typed `paneGet(paneId, socketPath?)` wrapper over `pane.get`
with `{ pane_id }`, returning the generated `PaneInfo`; reuse it for
scroll metadata.
- Use it for pane context, chat input checks, collapsed Codex queue
checks, image uploads, push title lookup and on-demand Claude
background-task discovery.
- Keep the previous optional-pane behavior in input checks. Herdr 0.9.3
returns `pane_not_found` with the existing message for unknown panes, so
the same HTTP helpers and envelopes continue to apply.
- Add unknown-pane HTTP contract cases for commands and files; the
existing image unknown-pane contract also passes.

`terminalInfoFor` still needs snapshot layout rects. Conversation
readers still need peers to disambiguate Devin, Codex and OmO transcript
identities, so `server/conversation.ts` remains unchanged. Global
collector, completion normalization, agent name allocation and cache
recovery retain their snapshots.

No HTTP/WS shapes, endpoints, browser source, generated types or agent
settings changed.

## Validation

All checks ran in
`/home/devswha/workspace/herdr-web-ui-wt/refactor-pane-get-lookups`.

- PASS: `bun install`.
- PASS: `bun run check fast` - workflow syntax, generated types,
typecheck and build passed; 2,285 unit tests passed, 7 existing
platform/fixture skips, 0 failures.
- PASS: `bun run check run bun test --timeout 15000
./server/api.contract.test.ts ./server/submit.contract.test.ts
./server/pending-input.contract.test.ts ./server/herdr/client.test.ts` -
180 tests passed, 0 failures.
- PASS: `bun run check run bun test --timeout 15000
./server/codex.contract.test.ts ./server/omp.contract.test.ts
./server/opencode.contract.test.ts
./server/claude-session.contract.test.ts` - 19 tests passed, 0 failures.
- PASS: `git diff --check`.

The source-state verification reports and full logs are preserved under:

- `/tmp/parity-qa-H6/fast/report.json`
- `/tmp/parity-qa-H6/contracts/report.json`
- `/tmp/parity-qa-H6/conversation-contracts/report.json`

### Isolated real-surface smoke

Built client served on port 7457 with a temporary app state directory
and isolated Herdr session `herdr-web-ui-qa-H6`.

- Opened the real chat pane in Playwright/Chromium at 1280 x 800 and 390
x 844; screenshots visually inspected and phone page had no horizontal
overflow.
- Sent a command from web chat while Herdr's TUI was concurrently
attached in a PTY; exact output appeared in the TUI and socket output
event.
- Typed a command through the Herdr TUI; the same output appeared in web
chat.
- `curl -sS -i -X POST http://127.0.0.1:7457/api/pane/image -H
'Content-Type: application/json' --data
'{"pane_id":"w1:p1","content_type":"image/png","data_base64":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="}'`
returned HTTP 200 and a project-local PNG path.
- No Playwright page errors.

Screenshots:

- `/tmp/parity-qa-H6/desktop-chat.png`
- `/tmp/parity-qa-H6/phone-chat.png`
- `/tmp/parity-qa-H6/phone-reverse.png`

Supporting evidence:

- `/tmp/parity-qa-H6/image-curl.txt`
- `/tmp/parity-qa-H6/tui-web-to-herdr.txt`
- `/tmp/parity-qa-H6/tui-herdr-to-web.txt`
- `/tmp/parity-qa-H6/output-events.json`
- `/tmp/parity-qa-H6/pane-list.json`
- `/tmp/parity-qa-H6/session-snapshot.json`
- `/tmp/parity-qa-H6/pane-get-schema.json`
- `/tmp/parity-qa-H6/qa-report.md`

Herdr RPCs used: `pane.get` for converted metadata reads; retained
`session.snapshot` for layout/global/peer needs. QA used
`workspace.create`, `pane.read`, `pane.send_text`/`pane.send_keys`,
`agent.prompt` with the existing shell fallback, `events.subscribe` with
exact output matches, plus CLI `pane list` and `api snapshot`.

### Cleanup and limits

Browsers, web service, both owned TUI processes, QA Herdr session and
persistent service monitors stopped. `server stop` exited 0; the
temporary QA root and uploads were removed. Evidence under
`/tmp/parity-qa-H6/` was retained. Successful check runners cleaned
their isolated sessions.

No browser source changed, so no UI regression suite was required beyond
the requested smoke. The large `server/index.ts` LSP diagnostic request
timed out, but full TypeScript checking passed. Optional gate-reviewer
spawning was unavailable at the task nesting limit; the implementer
reviewed the full diff. No product failures or open behavior issues
found.

* feat(settings): show herdr's agent integrations read-only (#722)

## What changed

- **`GET /api/integrations`** wraps herdr's `integration.list` RPC
(herdr 0.9.3) via `integrationList()` in `server/herdr/client.ts`, one
connection per call. It answers `{ integrations: AgentIntegration[] }`
in herdr's order; any other method is `400 method_not_allowed`.
`AgentIntegration` / `IntegrationsResponse` are hand-written in
`shared/protocol.ts` (the saved schema predates the RPC; `state` is
widened with `(string & {})`), with the route catalogued there.
- **Remote PCs:** it is a plain GET like `/api/agents`, so it joins the
bridge path list and `MACHINE_PROXY_PATH` (`machines.test.ts` asserts
it). The client is `fetchIntegrations(machineId)` in `src/lib/api.ts`,
bound in `useMachineApi()`. The Settings dialog renders outside a
`MachineContext`, so the page shows the local PC; a remote PC's list is
reachable through the proxy but no UI picks a PC yet.
- **Settings → Agent integrations** (new page, `Plug` icon, before
About) built from `SettingsGroup`/`SettingsRow`: agents found on the
PC's PATH first, then a "Not found on this PC" group. Installed rows
show a check. Missing or outdated rows show `herdr integration install
<label>` (the label is the CLI name, e.g. `antigravity-cli`) with a Copy
button using `copyText` and the select-the-text fallback.
- **Read-only by design:** nothing calls
`integration.install`/`integration.uninstall`, and there is no
install/uninstall button; installing writes into an agent's own
hooks/settings, which stays the user's call in a terminal.
- i18n ko/ja/zh, demo answer in `site/demo/transport.ts` (one of each
state), the page added to `scripts/settings-pages-demo-regression.ts`,
CHANGELOG line under Unreleased → Added.

## How it was checked

- `bun run check fast` → PASS on 81becc7 (workflow syntax, generated
types, typecheck, build, unit tests: 2285 pass, 0 fail)
- `bun run check run bun test --timeout 15000
./server/api.contract.test.ts` → PASS (99 pass, 0 fail) (new case:
`/api/integrations` equals `integration.list` from the isolated herdr;
`claude` present; states in the known set; POST refused)
- `bun scripts/settings-pages-demo-regression.ts` → PASS (every page
incl. Agent integrations fits 390px/320px in en/ko/ja/zh; Back/history
cases pass)
- Manual QA, isolated session `herdr-web-ui-qa-D` with
`HOME`/`XDG_CONFIG_HOME` in a temp dir, web server on :7455, Playwright
Chromium at 1280px and 390px:
- before: 17 rows, all "Not installed", 0 install/uninstall buttons,
Copy put `herdr integration install pi` on the clipboard, no page
errors, no sideways scroll; only `GET /api/integrations` requests.
- acted in herdr: Enter in herdr's own TUI integrations tab (TUI client
attached to the same session) installed claude + codex; `herdr
integration install opencode` from the CLI; codex's temp hook marked v1
to get `outdated`.
- after reopening the page: claude/opencode "Installed" with a check,
codex "Installed, but older than this herdr" + its command, matching
`herdr integration status` (`claude: current (v10)`, `codex: outdated
(v1 < v8)`, `opencode: current (v13)`).
- Screenshots (local): `/tmp/parity-qa-D/before-desktop.png`,
`before-phone.png`, `after-desktop.png`, `after-phone.png`;
TUI/CLI/snapshot evidence beside them.

## herdr RPCs used

`integration.list` only.

## Known limits

- herdr's `integration.list` omits Letta (experimental in `herdr
integration status`), so the page does too.
- The page reads once per opening; it does not poll. Reopen it after
installing.
- The Settings page shows the local PC only (see Remote PCs above).



Remote PCs: an SSH PC whose bridge predates this endpoint shows a plain
"not offered yet" message; it gets the page with the next remote runtime
release (no bundle is published here).

* fix(tabs): preserve numeric custom names (#723)

## Change

A tab label is automatic only when empty or equal to its current place,
so a numeric custom label survives closing an earlier tab. Adds unit
coverage and an Unreleased Fixed entry.

## Validation

- bun run check fast — PASS (workflow syntax, generated types,
typecheck, build, unit suite).
- bun test ./src/lib/tabName.test.ts — PASS (5 tests).
- Isolated herdr session herdr-web-ui-qa-H5, 1280px Chromium capture:
/tmp/parity-qa-H5/tab-strip-desktop.png shows label 3 after the earlier
tab closed.
- herdr api schema --json confirms tab fields include label and stable
number, without a custom-label marker. Repro used tab.create, tab.list,
tab.close, and pane.split; remaining state was w1:t2, number:2, label:3.

No known limitations.

* feat(palette): list and run herdr plugin actions (#720)

## What changed

The web UI can list and run the manifest actions of the herdr plugins
installed on a PC, as herdr's own TUI does.

- **Server**: `GET /api/plugins/actions` (`plugin.list` +
`plugin.action.list`, grouped by plugin with its enabled state; actions
for another platform are left out, command lines stay on the server) and
`POST /api/plugin/action { plugin_id, action_id, pane_id? }`
(`plugin.action.invoke`). Wrappers in `server/herdr/client.ts`, shapes
and route catalogue in `shared/protocol.ts`, both routes added to the
remote-PC proxy allowlist and to the bridge-token paths, answers in
`site/demo/transport.ts`.
- **Palette**: a **Plugin actions** group runs an action against the
selected pane. The row shows "Running…" and the palette stays open until
the server answers; a failed command says why above the list
(`role="alert"`); a pane the action opened and focused is selected.
- **Settings → About**: a small read-only "herdr plugins" group (name,
version, actions, Enabled/Disabled). It is not drawn when no plugin is
installed.

## What herdr does, measured on 0.9.3

- `plugin.action.invoke` takes each `PluginInvocationContext` field as
given and fills a missing one from **its own focus**, not from the pane
named: `focused_pane_id` alone came back with the focused workspace's ID
and label, and an unknown pane ID was accepted. So the server validates
the pane against a snapshot and sends workspace, tab, labels, cwd, agent
and status itself.
- Invoke answers while the command is still `running`. The route polls
`plugin.log.list` for that log entry for up to 5 s, so the browser gets
`succeeded`, `failed` (exit code and the end of stderr) or `running`.
- **Plugin panes**: the invoke answer names no pane, but an action can
open one by calling `herdr plugin pane open` (this repository's own
`phone` action does). `opened_pane_id` is a pane that did not exist
before the invoke and that herdr focuses after it; the palette selects
it.
- `plugin.action.list` includes a disabled plugin's actions and other
platforms' actions; herdr refuses them with `plugin_disabled` /
`platform_unsupported`. The palette offers neither.

herdr RPCs used: `plugin.list`, `plugin.action.list`,
`plugin.action.invoke`, `plugin.log.list`, `session.snapshot`.

## How it was checked

| Command | Result |
|---|---|
| `bun run check fast` | PASS (2288 pass, 7 skip, 0 fail; typecheck
included) |
| `bun run check run bun test --timeout 15000
./server/plugins.contract.test.ts` | PASS (8 pass, 0 fail) |
| `bun run check run bun scripts/file-viewer-regression.ts` (drives the
palette over Settings and a preview) | PASS |
| Browser + TUI QA driver on an isolated herdr session (own
`XDG_CONFIG_HOME`), Chromium via playwright-core | PASS, 18 of 18 checks
|

The contract test links a plugin from a temp directory into the run's
own herdr config and unlinks it afterwards. herdr keeps linked plugins
per user, not per session, so the suite skips itself when the herdr
under test uses the user's own `~/.config/herdr` (the shared
`herdr-web-ui-test` session); `bun run check` and CI give it a config
directory of its own.

QA evidence (local paths on the build PC, not committed):
`/tmp/parity-qa-C/`
- `01-desktop-palette-group.png`, `02-desktop-in-flight.png`,
`04-desktop-failed-action.png`, `05-desktop-opened-pane-selected.png`,
`06-desktop-settings-plugins.png` at 1280px
- `09-phone-palette-group.png`, `10-phone-in-flight.png`,
`11-phone-failed-action.png` at 390px
- Web → herdr: `marker-from-web.json` (the context the plugin command
received: the web's selected pane, `invocation_source: herdr-web-ui`),
`tui-02-after-web-marker.txt` and
`socket-02-pane-list-after-web-marker.json` (the pane the action
renamed), `tui-03-after-web-open-board.txt` and
`socket-03-snapshot-after-web-open-board.json` (the plugin pane,
focused)
- herdr → web: `herdr-side-invoke.json` +
`07-desktop-after-herdr-side-invoke.png` (a plugin pane opened from
herdr's CLI appears in the web),
`08-desktop-plugin-disabled-in-herdr.png` (a plugin disabled in herdr
leaves the palette)
- `qa-results.txt`, `qa-driver.ts`, `qa-plugin.toml`

## Known limits

- Where an action's pane opens is the plugin's choice: `herdr plugin
pane open` uses herdr's focus unless the command passes `--target-pane`
or `--workspace`. The context is correct either way (`HERDR_PANE_ID` and
the context JSON name the web's pane).
- A popup-placement plugin pane has no pane ID and shows only in a herdr
TUI client; the web cannot select it.
- An action whose only context is `selection` is not offered: the web
has no selected text to hand over. Without a selected pane only `global`
(or context-less) actions are offered.
- A command still running after 5 s is reported as `running` and the
palette closes; its later failure is only in `herdr plugin log list`.
- Installing, enabling, disabling and removing plugins stay with herdr.
A remote PC on an older bridge answers 404 and the group is simply
absent there.
- Remote PCs get the routes with the next `remote-vN` bundle; none was
built here.

* fix(collector): refresh sessions on herdr lifecycle events (#725)

## Change

Fix audit H4: structural changes made in herdr's TUI now invalidate the
web roster/layout through the existing `session-changed` frame instead
of waiting for five-second polls.

- Subscribe to workspace, worktree, tab, pane and layout lifecycle
invalidations.
- Coalesce event bursts in the existing 500 ms window; snapshot retry
timers cannot delay browser invalidation.
- Reconcile authoritative pane IDs, closing and reopening the complete
per-pane status subscription after moves.
- Retain browser, machine and collector polls and recovery/health
checks. No UI or wire-shape changes.

## Validation

- PASS `bun install` (Bun 1.4.2).
- PASS `bun run check fast`: workflow syntax, generated types,
typecheck, build and unit suite, exit 0.
- PASS `bun run check run bun test --timeout 15000
./server/collector.contract.test.ts`: 3 tests, 0 failures; socket
`workspace.rename`, `tab.rename`, `pane.zoom` each produced WS
`session-changed` within 591-601 ms. Isolated herdr 0.9.3.
- PASS `bun run check run --build bun scripts/ui-regression.ts`: built
real client and existing desktop/mobile regression suite, exit 0. Only
lock refusals were retried, in bounded 30-second intervals; no test
failure was retried.
- PASS concurrent real browser and TUI on isolated `herdr-web-ui-qa-H4`,
bridge port 7456, temporary state: web tab rename appeared in TUI and
socket snapshot; TUI workspace rename painted in 703 ms; TUI tab rename
push arrived in 546 ms and painted in 650 ms.
- APPROVE read-only Codex `gpt-6.1-sol`, `xhigh`, fast-tier review under
an 8 GB memory cap: no must-fix findings
(`/tmp/parity-qa-H4/review.txt`).

## Evidence

Local evidence root: `/tmp/parity-qa-H4/`.

Screenshots: `desktop-web-to-tui.png`,
`desktop-tui-workspace-rename.png`, `desktop-tui-tab-rename.png`
(1280x800), `phone-tui-tab-rename.png`, `phone-tui-workspace-rename.png`
(390x844).

Timings/state/receipts: `timings.json`, `snapshot.json`,
`pane-list.json`, `tui-screen.txt`, `tui.typescript`, `qa-report.md`,
`cleanup.txt`, `fast-report.json`, `contract-report.json`,
`browser-report.json`.

QA setup RPCs: `workspace.create` and `tab.create` with `focus:false`,
`workspace.focus`, `session.snapshot`. Reverse actions used actual TUI
Ctrl+B, Shift+W/Shift+T rename dialogs, not API substitutes.

## Limits and cleanup

Screenshots are local paths, not hosted attachments. The first QA bridge
startup used an unset process environment and briefly reached the
default socket; attach was refused as `attach_held`, without takeover.
It was stopped; all retained captures came from a fresh owned browser
context against the explicitly isolated socket.

The browser, port-7456 bridge, TUI and QA herdr were stopped; temporary
QA state was removed. Both check runners cleaned their owned sessions.
No merge, main push or tag. Remote bridge deployment and GitHub CI are
not claimed by these local checks.

* feat(terminal): add native pane history search (#729)

## Change

Add **Find in terminal** to the header's More menu and Mod+Shift+F. The
find bar searches
herdr's stored pane history, shows the native match ordinal/count and
supports next/previous,
Enter, Shift+Enter and Escape. Keyboard focus remains in Find across
Chat transitions and
deferred upload completion; find text is never delivered to the
terminal.

`POST /api/pane/find` owns the HTTP contract in `shared/protocol.ts`,
validates requests and
uses fresh herdr RPC connections. Remote-PC routing is allowlisted and
machine-bound. Add
ko/ja/zh translations, explicit demo unavailability, a changelog entry
and native/browser
regression coverage. xterm scrollback remains zero; native attach
redraws display the match.

## Validation

- `bun install` — PASS.
- `bun run check fast` — PASS: workflow syntax, generated-type
freshness, typecheck, build
  and unit tests. Report:

`node_modules/.cache/check/fb6199c3-9334-4764-b95e-97dbf943080b/report.json`.
- `bun run check run bun test --timeout 15000
./server/find.contract.test.ts` — PASS,
8 tests: found, next/previous, not found, punctuation, malformed query,
unknown pane,
navigation validation and a real native revision race with no stale
scroll. Report:

`node_modules/.cache/check/73624152-7932-4eb7-a90c-8f068b246879/report.json`.
- `bun run check run --build bun scripts/ui-regression.ts` — PASS,
including the new
find regression, real keyboard focus, Chat-to-Find, touch-menu access
and a delayed real
  image upload. Report:

`node_modules/.cache/check/bef52677-5a76-4903-9af4-9973b4c51b86/report.json`.
- `UI_EVIDENCE_DIR=/tmp/parity-qa-G bun run check run --build bun
scripts/pane-find-regression.ts`
  — PASS as a focused check before the full suite.
- Codex `gpt-6.1-sol`, xhigh, fast tier — final APPROVE after focus/race
findings were
fixed and regression-tested.
`/tmp/parity-qa-G/codex-review-upload-followup.txt`.

Final manual QA used owned `herdr-web-ui-qa-G`, port 7454 and a
temporary app state directory.
Print `FIND_QA_G_UNIQUE` followed by 320 lines: GUI search finds 1 of 1
and displays it.
A concurrently attached herdr TUI shows the same match; typing
`TUI_REVERSE_QA_G` there
updates the web terminal. Desktop 1280px and phone 390px captures in
both themes:

- `/tmp/parity-qa-G/desktop-find.png`
- `/tmp/parity-qa-G/desktop-find-light.png`
- `/tmp/parity-qa-G/phone-find.png`
- `/tmp/parity-qa-G/phone-find-light.png`

RPC/TUI evidence: `/tmp/parity-qa-G/reviewed-evidence.json`,
`reviewed-tui-found.txt`,
`reviewed-tui-reverse.txt`, `native-pane-list.txt`,
`native-snapshot.json`, `history-read.json`
and `search-schema.json`. Cleanup receipts:
`/tmp/parity-qa-G/cleanup-receipts.json`.

## Native API and limits

Use `pane.get`, `pane.copy_motion`, `pane.copy_search` and
`pane.scroll`. Copy mode nee…
usergood added a commit to usergood/saurons-eye that referenced this pull request Oct 12, 2026
* fix(ci): gate deployment on source-bound verification (#692)

## Change
- Require reusable CI and the site build to succeed for the same SHA
before Pages publishes the website, demo, or installers. Pin checkouts,
keep deployment serialization, and fail closed on stale-main/API errors,
including manual runs.
- Strengthen existing `bun run check`: durable source-bound JSON and
command logs, source/dist invalidation, interrupted/not-run/skipped
states, shared lock for `run`, and `run --build` for browser
prerequisites.
- Retain reports on success and failure. Capture isolated failure
screenshots, bounded console errors, and optional traces without
DOM/network capture in the main UI and prompt-dock regressions.
- Typecheck six critical tooling/demo entrypoints without relaxing
strictness. Document parallel ownership, genuine-dependency-only stacks,
squash/restack validation and explicit authority.

## Validation
- `bun run check fast`: passed after lifecycle fixes; 2242 tests passed,
7 skipped, 0 failed (before the final fail-closed correction, which has
focused coverage below). Includes workflow syntax, generated types, both
typechecks and app build.
- Final focused check/report/browser-evidence/Pages/browser-runner
tests: 39 passed, 233 assertions; both typechecks passed.
- Actual CLI lifecycle smoke: success/failure logs, build provenance,
source/dist mutation invalidation, competing-run lock refusal, SIGTERM
report and subsequent lock release passed in a disposable fixture
repository with a fake herdr executable.
- Actual Playwright synthetic failure: PNG, console metadata, private
artifact modes and trace without DOM/network capture captured from an
isolated fixture.
- Independent policy, deployment/evidence and report reviews completed.
Fixed interrupt cleanup ordering, inherited-output draining,
shared-report overwrite, unavailable source/build identities, and
bounded trace teardown. Actual CLI smoke also covered stubborn
descendants, shared-report lock refusal and source-identity loss.
Required GitHub CI remains authoritative for final SHA f44e9cc.

## Repository settings and remaining credential boundary
- Added `Native Windows install` to required main checks; retained
strict checks, admin enforcement, conversation resolution and optional
approval count. Latest 30 completed CI runs had 27 Windows successes,
one cancellation, and two fork runs awaiting approval without jobs.
- This PR does not provision an agent account or GitHub App credential.
Non-admin implementation credentials require separate operator
provisioning; no claim that current administrator credentials have been
replaced.

## Coordination record
Owner: coordinating maintainer session. Scope: verification/deployment
tooling and operating documentation. Start SHA:
b87d9d50019cdb87422415f7650ad29285a832cc. Branch/worktree:
worktree/rapid-stone-f62b. Dependencies: existing ci.yml/check
entrypoints only; no stacked PR dependency. Integration and release are
explicitly authorized by the user, subject to required checks and
unresolved-review blocking.

* chore(release): prepare 0.4.4 (#694)

## Change
Prepare app release 0.4.4 from main after #692, using the existing
release process only.

- Bump package.json and herdr-plugin.toml together.
- Move the existing Unreleased entries under 0.4.4 (2026-10-09 UTC),
preserving all PR links and external contributor credits.
- Add matching English, Korean, Japanese and Chinese release highlights
(3 new, 1 improved, 4 fixed; every line <=90 characters).
- No tags are created by this PR. The existing Release workflow must
validate the exact main SHA before publishing.

## Validation and review
- `bun scripts/release-notes.ts 0.4.4`: passed, generated expected
release body.
- Release-notes tests: 27 passed, 397 assertions, 0 failures.
- `bun install --frozen-lockfile`: passed without dependency changes.
- Reviewed the release metadata diff; all prior summary entries are
unchanged. The only non-metadata change is the one-line recorder
synchronization fix documented below.
- Audited release-range PR authors and existing changelog credits;
implementation PR #692 is linked.
- Required CI must pass on this PR before squash merge; no administrator
bypass.

Owner: release coordinator (explicitly authorized by user). Start SHA:
ceae7a259fa6f0d16ece3e56abb2178e752541dd. Scope: four release metadata
files and one release-blocking contract-test synchronization line.
Branch: chore/release-0.4.4. Dependencies: merged #692; no open stack.

## Observed release blocker
Website run
[37973801948](https://github.com/devswha/herdr-web-ui/actions/runs/37973801948)
correctly skipped deployment when the existing pending-input contract
test read `barrier` before the recorder observed its final Enter. A
WebSocket receipt is not a receipt from the separate recorder process.

Commit fe09359 adds the existing bounded `waitBytes("barrier\r")` before
the exact-byte and no-enqueue assertions, matching adjacent tests. No
production behavior, expectation, timeout, or skip condition changed.
This tiny prerequisite stays in the release PR rather than creating an
artificial release stack; it is a separate commit for review. No
changelog entry is needed for an internal test synchronization
correction.

Verification on the isolated herdr: full pending-input file **37
passed**, then the affected case **10/10 passed** with `--rerun-each
10`. Source-bound reports were preserved. Required CI is rerunning for
the new PR SHA; no failed check is bypassed or blindly rerun.

* fix(terminal): keep a tab out of use updating; pausing it is a setting (#699)

## Change

**Problem.** Since 0.4.4 (#660), a desktop tab detaches its pane one
second after the user switches to another window, so that herdr's own
TUI gets the pane's size back. The tab then stops updating until it is
clicked again. To someone who does not run herdr's TUI next to the
browser, this looks like the terminal froze. It also happens on a second
monitor where the tab stays in view.

**Change.**
- A tab that is out of use keeps its pane and its output again, as in
0.4.3. It still sends no resize and reconnects with `keep_size`. Only
the release is affected.
- **Settings → Terminal → Pause in another window** (`releasePaneAway`,
off by default) turns #660's release back on, unchanged, for people who
also work in herdr's own TUI on the same PC.
- The herdr API has no read-only way to tell whether a TUI client shows
the pane: the snapshot has no client field, and `no_foreground_client`
comes only from `client.window_title.set/clear`, which change the user's
title. So the choice stays with the user.

@jiunshinn: #660 made a desktop tab stop updating a second after the
user switches windows, and we had a report that this reads as a frozen
terminal. This PR makes that release opt-in, off by default. A follow-up
will make the setting switch the tab to a read-only `herdr terminal
session observe` stream instead of pausing. I measured on 0.9.3 that the
observer leaves herdr's TUI at its own size (44x37 stays 44x37, while an
attach makes it 150x50), so the tab keeps updating and the TUI is not
cut off. Feedback welcome.

## Validation

- `scripts/chat-size-regression.ts` against a `bun run check run
--build` herdr: 20/20 PASS. The new check (default settings, window
blurred, clock run past the 1 s release delay) confirms there is no
`detach`, no paused banner, and the pane's `pty-data` still arrives. The
existing #660 checks run with the setting on and still pass.
- `bun run check fast`: workflow syntax, generated types, typecheck,
build, unit tests all ok.
- `src/lib/settings.test.ts`: the setting is off by default and only a
boolean `true` turns it on.
- Settings → Terminal opened in a browser against the isolated test
session: the toggle shows up off, turns on, and is saved to
`herdr-web-ui:settings`. Korean, Japanese and Chinese strings were
added, and `i18n.test.ts` passes.

* feat(terminal): a tab out of use watches its pane read-only instead of pausing (#700)

## Change

**Problem.** #699 made #660's release opt-in: with the setting on, a
desktop tab out of use detaches its pane so that herdr's own TUI gets
the pane's size back, and then the tab freezes until it is clicked.
People who run herdr's TUI next to the browser had to pick one of two
problems: a frozen web terminal, or a TUI cut off at the browser's size.

**Fix.** herdr ≥ 0.7.2 has a read-only observer, `herdr terminal session
observe <pane> --cols --rows`. Unlike `terminal attach`, it does not
take ownership of the pane's size. I measured this on 0.9.3 with a real
TUI client and a split pane at 44 columns:

| state | pane size (`stty size`) |
|---|---|
| TUI only | 37 44 |
| browser tab in use (attach) | 37 145 |
| browser tab out of use, **watching** | **37 44** |
| browser back in use | 37 145 |

With the setting on, the tab now detaches as before, then watches the
pane instead of pausing. The terminal shows "View only while you use
another window · click to type". Focus or a pointerdown unwatches and
attaches again at the tab's size. A grid change starts the view again at
the new grid. A released tab that moves to another pane watches that
one.

- **Wire** (`shared/protocol.ts`, feature `"watch"`): client `watch
{pane_id, cols, rows}` / `unwatch {pane_id}`; server `watch-data
{pane_id, data}` / `watch-end {pane_id}`. A watch is not an attach:
there is no input, resize, ACK or input-ready, and it never touches
`attachments`, claims or `pane-geometry`, so a phone can keep its attach
while a desktop watches.
- **Server** (`server/watch.ts`): one observer per (connection, pane),
started with plain `Bun.spawn`. It is not the PTY sidecar.
`HERDR_SOCKET_PATH` is pinned the same way the attach does it. Frames
are NDJSON; `bytes` is base64 of ANSI, and herdr labels it
`encoding:"ansi"`. They are decoded with a streaming UTF-8 decoder. The
observer is killed on unwatch, a replacing watch, WS close, a
slow-client stop, device revoke and `stop()`.
- **Fallbacks:** an older bridge has no `"watch"` feature, and a
`watch-end` can arrive when the pane is gone or herdr has no observer.
In both cases the tab stays paused on its last screen, as before. The
site's demo answers `watch` with `watch-end`; its page never releases
anyway.
- **Setting renamed** to describe the situation rather than the
mechanism: **Use alongside herdr's own window** (still off by default,
same `releasePaneAway` key, not yet released).
- The herdr API has no read-only way to detect a TUI client, so this
stays a setting. `herdr api schema --json` has no client listing, and
`no_foreground_client` only comes from `client.window_title.set/clear`,
which change the user's title.

## Validation

- `server/watch.contract.test.ts` (new, live herdr via `bun run check
run`), 6/6 passing: the `"watch"` feature is advertised; an unattached
pane streams real command output to an observe-role client while no
`terminal attach` process runs; unwatch kills the observer without a
`watch-end`; WS close kills it; a missing pane gets `watch-end`; invalid
geometry gets `invalid_geometry`.
- `server/watch.test.ts` (new, unit): NDJSON across chunk boundaries, a
UTF-8 character split across two frames, `terminal.closed`, unknown
records.
- `src/lib/ws.test.ts`: a watch goes out after the snapshot and again
after a reconnect; it is not re-sent after `watch-end`; a reconnect to a
bridge without the feature ends it.
- `scripts/chat-size-regression.ts` against `bun run check run --build`,
21/21 PASS. The release checks now expect the view-only banner and a
`watch` frame. New: once the phone leaves and no attach runs, the window
still receives the pane's output through `watch-data`.
- End to end, by hand, with a real herdr TUI in tmux on a throwaway
session: the sizes in the table above, and `echo LIVE-WHILE-WATCHING`
showed up in the browser while it watched.
- `bun run check fast`: all ok. New strings are in ko/ja/zh.

* fix(claude): certify background counts against scanned file versions (#701)

## Change
Fix R1 from the independent v0.4.4 review: a concurrent parent append
could be certified under the newer post-scan signature, freezing a stale
count. Cache only identical pre/post scan versions and dependencies that
were quiet before the scan. Parent identity and folder replacement now
participate in invalidation.

## Validation
Reproduced the race 3/3 before the fix; all 3 after-fix probes converge.
Focused public behavior regressions cover replacement and stable reads.
The complete remediation rebased on current main passes `bun run check
full`: 2,259 unit passes (7 platform skips), 323 integration passes, all
20 browser scripts and the demo build. This PR also requires its own CI
before merge.

## Ownership
owner: devswha via Amp
scope: R1 count-cache certification only
branch/worktree: fix/claude-count-scan / dedicated
release-review-remediation worktree
start SHA: 3a5b05068a5a2c623cb9dacd372e351a61ad2470
dependencies: none
verification: local combined full check passed; PR CI pending
status: ready for review

No code adapted from another project.

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(claude): retry incomplete process identity at bounded intervals (#702)

## Change
Fix verified R3 from the v0.4.4 review: an initial missing PID or
process-start lookup used to stay unknown forever. Retry incomplete
identity at the existing bounded interval, including resolver-only
callers. A recovered process boundary correctly marks tasks from the
previous process lost; no timestamp is invented.

## Validation
Reproduced the sticky unknown identity before fixing. Public status
tests cover a missing PID, missing start and resolver-only recovery.
Focused claude-subagents tests pass on current main. The combined
remediation full run on the latest terminal changes and metadata
identity fix passed 2,259 unit tests, 323 integration tests and all 20
browser scripts plus demo build. This PR requires its own CI and native
gates.

## Ownership
owner: devswha via Amp
scope: R3 process identity recovery
branch/worktree: fix/claude-process-identity / dedicated remediation
worktree
start SHA: 36209a0 (main after #701)
dependencies: none; based on latest main
verification: focused tests passed; PR CI pending
status: ready for review

Changelog PR links will be finalized in the release preparation PR. No
externally adapted code.

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(claude): retain observed rest across delayed transcript discovery (#703)

## Change
Fix verified R2: delayed transcript discovery used to erase the observed
working-to-rest transition, leaving a turn with background work at DONE
instead of BG. Associate independently observed rest times with the
discovered prompt; keep the first eligible deadline across automatic
resumes. A positively replaced session/process clears the old wait
evidence, including equal-count transitions. Initial
discovery/refinement does not reset it.

This does not suppress or retract an initial DONE notification emitted
before discovery; that remains a separate product policy.

## Validation
Reproduced early vs delayed discovery before fixing. State/concurrency
design was checked with Oracle. Tests cover delayed discovery,
first-deadline preservation, initially resting panes and positive
session replacement. Existing live contract tests check exact frames and
one final push; their injected clock and transcript timestamps now share
one time domain. All 9 contract tests and the complete combined full
check passed; focused unit checks pass again on latest main. This PR
must pass its own CI/native/remote gates.

## Ownership
owner: devswha via Amp
scope: R2 delayed BG state
branch/worktree: fix/claude-delayed-background / dedicated remediation
worktree
start SHA: d4fa866 (main after #702)
dependencies: integrated process-identity recovery
verification: local focused and combined full checks passed; PR CI
pending
status: ready for review

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(licenses): include the bundled TanStack Highlight license (#704)

## Change
Fix verified R4 from the v0.4.4 independent review: the distributed
notices omitted the bundled TanStack Highlight MIT text. Include the
exact installed upstream license and a focused regression comparing it
with the dependency license. No runtime behavior changes.

## Validation
The license test passes on latest main. Production and demo builds were
checked byte-for-byte against THIRD_PARTY_NOTICES.md. The combined
remediation with review corrections and the readline fixture input-order
fix passes the full local suite: 2,260 unit tests, 323 integration
tests, all 20 browser scripts plus demo build. This PR also requires its
own CI.

## Ownership
owner: devswha via Amp
scope: bundled Highlight license only
branch/worktree: fix/highlight-license / dedicated remediation worktree
start SHA: c824aab (main after #703)
dependencies: none
verification: focused license test and combined full suite passed; PR CI
pending
status: ready for review

The added text is the MIT license distributed by @tanstack/highlight;
implementation code is not adapted.

Co-authored-by: Amp <amp@ampcode.com>

* fix(deps): update source-map-js to patched 1.2.2 (#705)

## Change
Resolve the inherited high-severity source-map-js advisory identified in
the v0.4.4 release review. This is a lockfile-only 1.2.1 → 1.2.2 patch;
no dependency range or unrelated package changes.

## Validation
The original audit reported one high advisory. After the patch, frozen
install succeeds and bun audit reports zero vulnerabilities across 159
packages. The combined remediation full suite and production/demo builds
pass. Native CI and remote bundle/SSH validation must pass on this PR
before merge.

## Ownership
owner: devswha via Amp
scope: source-map-js patch only
branch/worktree: fix/source-map-security / dedicated remediation
worktree
start SHA: 814754d (main after #704)
dependencies: none
verification: frozen install, audit and combined full suite passed; PR
CI pending
status: ready for review

Co-authored-by: Amp <amp@ampcode.com>

* fix(claude): invalidate background counts at task age boundaries (#706)

## Change
Fix verified R6: an unchanged file signature bypassed the existing
one-day task age rule, so a warm badge kept a task that a direct read
had already expired. Include the age eligibility boundary in the watched
signature using the same clock as the poll. No new age policy or
configurable expiry is introduced.

## Validation
Before/after probes reproduced a warm count of 1 vs direct count 0;
after the fix both are 0. Public status regression covers both sides of
the age boundary without any file write. Focused claude-subagents tests
pass on latest main; the combined full suite with review corrections
passes 2,260 unit tests, 323 integration tests and 20 browser scripts
plus demo build. PR CI must pass separately.

## Ownership
owner: devswha via Amp
scope: R6 age-boundary invalidation
branch/worktree: fix/claude-task-age / dedicated remediation worktree
start SHA: 56d9ae0 (main after #705)
dependencies: integrated count-cache certification
verification: focused and combined full checks passed; PR CI pending
status: ready for review

---------

Co-authored-by: Amp <amp@ampcode.com>

* test(ci): gate releases on native session identity regressions (#709)

## Changes
Run existing Windows-native identity tests in reusable CI and add native
macOS Claude-store/process-start coverage. Release and Website callers
inherit the checks. Preserve existing platform coverage and all
assertions.

Also fix the verified readline fixture input race: prior browser
keystrokes could prefix the direct-RPC launch command. Send cleanup and
launch through the same browser input stream. Separate commits preserve
the independent changes.

## Validation and CI timing
Typecheck passed on this branch. Combined full local validation passed
(2260 unit, 323 integration, 20 browser scripts). Native behavior must
pass on the corresponding GitHub runners before merge.
Measured integration job queue 2s versus execution 485s; browser lane
462.9s and integration lane 208.7s already overlap. No test deletion,
relaxed deadline, new cache or gate reuse. Structural sharding remains
follow-up work.

## Ownership
- owner: devswha / Amp
- scope: native validation registration and verified browser fixture
race
- branch/worktree: test/native-release-validation / dedicated
release-review-remediation
- start SHA: d12a77c
- dependencies: none
- verification: local typecheck, full combined run, native and browser
PR CI
- status: ready for review and CI

Amp-Thread-ID:
https://ampcode.com/threads/T-01a12235-af16-73b1-ad0c-625b633f2697

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(chat): retain page-scoped Claude completion identities (#708)

## Change
Keep completion identities across settled/live transcript boundaries
within the requested page. Reparse live suffixes with a copy, so later
polls and resumed completions remain visible. Do not deduplicate across
pages.

## Validation
Regression reproduced before the fix. Restacked conversation suite: 78
pass. Combined remediation check full passed; required PR checks run on
this exact revision.

## Ownership
- owner: devswha / Amp
- scope: Claude completion identity cache and regression
- branch/worktree: fix/claude-completion-dedup in dedicated
release-review-remediation worktree
- start SHA: d12a77c (latest origin/main)
- dependencies: none
- verification: focused unit suite, required CI
- status: ready for independent review and CI

Amp-Thread-ID:
https://ampcode.com/threads/T-01a12235-af16-73b1-ad0c-625b633f2697

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix(chat): preserve trailing code whitespace in manual selections (#710)

## Change
Normalize CRLF only; preserve trailing source newlines in rendered code
for manual selection with highlighting on or off. Copy code still uses
the original source.

## Validation
Focused unit tests passed on this branch. The combined full check
passed, including file-viewer DOM/Range assertions for both highlighting
settings. Synthetic rendered screenshots were inspected:
- On:
https://ampcode.com/user-content/artifacts/d1a195977397e5703819cda7bdd33f742c8d4cf6dd62c538e9a6ccb498a01be0-file.png
- Off:
https://ampcode.com/user-content/artifacts/c3bca89c20ccadc70690e0984208a6274b8314387aec34c2b189d801e45060bc-file.png
Required CI runs again for this PR.

## Ownership
- owner: devswha / Amp
- scope: source whitespace fidelity and regression coverage
- branch/worktree: fix/code-selection-whitespace / dedicated
release-review-remediation
- start SHA: d12a77c
- dependencies: none
- verification: unit tests, browser DOM/Range and visual inspection, PR
CI
- status: ready for review and CI

Amp-Thread-ID:
https://ampcode.com/threads/T-01a12235-af16-73b1-ad0c-625b633f2697

---------

Co-authored-by: Amp <amp@ampcode.com>

* chore(release): prepare v0.4.5 and remote runtime v24 (#711)

## Release
Prepare patch v0.4.5 with matching package/plugin versions, dated
changelog, four-language update summaries and remote bundle version 24.

Includes independent review remediation #701–#706, #708–#710, plus
already merged #699/#700. All product PRs passed required CI on their
current base; native Windows and macOS validation is now inherited by
Release. All review threads are resolved.

Remote-v24 MUST publish all five checked archives and manifest before
dispatching Release for v0.4.5. Remote PCs will require Update bridge…
once. Never push the app v-tag manually.

## Validation
Metadata generation and frozen install/audit passed. A fresh complete
local check and required PR CI run on this integrated revision. Previous
combined check: 2260 unit, 323 integration, 20 browser scripts plus demo
build. Actual native jobs: 11 Windows and 37 macOS tests passed.

## CI investigation
Queue versus execution measured separately: integration/browser 2s/485s
before, 13s/493s after fixture correction. No speedup claimed. Official
ECR Debian mirror avoids verified Docker Hub pull throttling; browser
fixture now orders cleanup and launch with earlier input. Tests, support
platforms and exact-SHA release/deployment gates are retained. Browser
sharding or validation reuse are separate follow-up proposals.

## Limitations
Initial alerts emitted before transcript discovery cannot be retracted;
status events lack process identity. Completion dedup is page-scoped.
Real phone/Safari/PWA naming, live paid agents and delivered production
push timing remain unverified. Optional architecture cleanup is
excluded.

## Ownership
- owner: devswha / Amp
- scope: v0.4.5 release metadata and remote-v24
- branch/worktree: chore/release-v0.4.5 / dedicated
release-review-remediation
- start SHA: e6a7f81
- dependencies: all product remediation PRs merged
- verification: fresh check full, native CI, five platform bundle
checks, Release exact-SHA gate
- status: release candidate; publish only after gates pass

Amp-Thread-ID:
https://ampcode.com/threads/T-01a12235-af16-73b1-ad0c-625b633f2697

---------

Co-authored-by: Amp <amp@ampcode.com>

* docs(agents): drop the Session panel row, whose component is gone (#693)

The `Session panel` row in the WHERE TO LOOK table named four things:

- `components/SessionPanel.tsx`
- `lib/activity.ts`
- an `onActivity` prop on ChatView
- a `herdr-web-ui:session-panel` storage key

None of them exist. `grep -rn SessionPanel src/` matches this row and
nothing else; `onActivity` and `session-panel` likewise appear nowhere
but here.

`components/BackgroundTasks.tsx` is **not** a rename of that surface —
it is omo task runs polled from the server (`lib/omoTasks.ts`,
`OmoTask`), where the row described turns derived client-side inside
ChatView. Repointing the row at it would have been a plausible-looking
lie, so the row is deleted.

No behaviour change. Found by the V8 finding of the 2026-10-06
accessibility/visual audit.

Co-authored-by: Hako <devswha@gmail.com>

* ci: typecheck three more entry-point scripts (#696)

No code changes. `tsconfig.tools.json` covers six entry points; three
more that CI actually runs typecheck clean under it today and are simply
not listed:

- `scripts/build-remote-bundle.ts` (`build:remote`)
- `scripts/ssh-integration.ts` (`test:ssh`)
- `scripts/file-viewer-regression.ts` (the browser lane — AGENTS.md
notes `test:ui` skips it but CI runs it)

Adding them makes `bun run typecheck` fail on a missing or mistyped
import in any of the three, which is the failure class that let a bad
commit reach `main` green before.

## What is still uncovered, and why it is not in this PR

The one I most wanted is `scripts/ui-regression.ts` — the main browser
entry point. Its transitive closure carries 17 errors across five files:

```
7  scripts/notification-startup-regression.ts
4  scripts/mobile-viewport-regression.ts
3  scripts/alert-sound-regression.ts
2  scripts/ui-regression.ts
1  scripts/csp-violations.ts
```

Two of those are worth your eye regardless of the typecheck question:

- `ui-regression.ts:304` uses `Array.prototype.findLast`, which needs
`lib: ES2023`; the root tsconfig is `target: ES2022`. That is a config
gap rather than a script bug, and bumping `lib` is a project-wide call I
did not want to make inside a hygiene PR.
- `csp-violations.ts:72` — `Object is possibly undefined`.

`generate-protocol-types.ts` is also clean-except-for-one (an unused
local at :114), so I left it out rather than bundle a code change into a
PR promising none.

Fixing the 17 is a small, mechanical follow-up if you want the browser
lane covered — say so and I will do it as its own PR, or fold the `lib`
bump in separately if you would rather have the coverage than the
smaller diff now.

Co-authored-by: Hako <devswha@gmail.com>

* fix(a11y): never read a pane id out as the terminal's name (#695)

Follow-up to #633. That PR labels the terminal region with the pane's
title and falls back to the pane id:

```ts
paneId === null ? t("Terminal") : t("Terminal for {title}", { title: title ?? paneId })
```

`title` is null until the snapshot that names the pane arrives, so a
pane opened on that gap announces itself as **"Terminal for
p_1a2b3c4d"** — a screen reader reads the id aloud as the name of the
product's central surface.

A pane with nothing to call it now takes the grid's own kind, which is
the name the idle grid (`paneId === null`) already uses.

The rule is pulled into `src/lib/terminalLabel.ts` so it can be pinned
by a test — this is the one a11y surface in the app with no unit
coverage at all, and the failure mode (a fallback string) is exactly
what a test should hold.

```
bun test ./src/lib/terminalLabel.test.ts   2 pass
bun run typecheck                          clean
bun run test:unit                          1720 pass / 0 fail
```

No new translation keys: both strings already ship for #633.

**Scope note, not fixed here.** `displayPaneTitle()`
(src/components/Sidebar.tsx:55) itself falls back to `pane.pane_id` when
a pane has no label and no derivable title, so a genuinely untitled pane
can still carry an id here — and it shows one in the sidebar today as
well. Distinguishing "no title yet" from "no title to be had" needs a
nullable accessor beside `displayPaneTitle`, which changes that
function's contract and is a wider change than this. Happy to do it as a
follow-up if you want it; say the word.

---------

Co-authored-by: Hako <devswha@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(security): a session cookie with no Origin is cross-site (#697)

The S6 finding of the 2026-10-06 audit. `sameOrigin()` returns `true`
when the `Origin` header is absent, so a cookie-bearing request whose
`Origin` went missing — or was stripped by something in front — passes
the origin gate.

A browser always attaches `Origin` to a cross-origin state-changing
request, and the WS upgrade already carries the device cookie, so this
is not a live browser hole: `SameSite=Strict` covers the case. It is
defence in depth, and the audit rated it Low for that reason.

**What I am not claiming:** I did not find an exploit chain, and I am
not asking you to treat this as one.

## The three paths that make this hard

This is the second attempt. The first was reverted in `e755194` because
tightening `sameOrigin` broke the documented wire contract. These three
carry a session cookie and state no `Origin` in exactly the cases that
need them:

| Path | Why it cannot state one |
|---|---|
| `GET /ws` upgrade | a browser cannot attach the custom mutation header
to a WebSocket handshake |
| `DELETE /api/auth` | sign-out; it manages the sender's own session |
| `POST /api/push/subscribe` | the same session's own push subscription
|

So each is exempted by path, and each is now pinned:

- `server/auth.test.ts` — 11 cases: the three exempted, the two cookies
alone, the `x-herdr-machine` proof, a CLI with no cookie, and a browser
that does state an origin.
- `server/open-access.test.ts` — the same thing through the HTTP seam: a
watch device with no `Origin` is refused `invalid_origin`, and with the
header it gets past the origin gate and is refused `read_only` instead —
i.e. the origin check stopped being the thing answering.

```
bun test ./server/auth.test.ts ./server/open-access.test.ts    21 pass / 0 fail
```

I also ran a throwaway probe of those 11 cases against `main` without
this change: 5 of them flip to `true`, so the tests are not passing
vacuously.

## Notes

- `machine-security.ts` is byte-identical to the copy this was written
against, so the change carried over without a conflict.
- `pty-compare.ts` was in the same working tree and is deliberately not
included — unrelated.
- If you would rather this sat as a recorded observation than a change
(the audit already lists S6 as `"recorded"`), close it and I will drop
it. I would rather ask than assume.

---------

Co-authored-by: Hako <devswha@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(machines): show an unreachable PC as reconnecting after a failed bridge update (#716)

## Problem

A registered remote PC that is switched off kept showing **Bridge update
needed** / "needs a bridge update to reconnect" with an **Update
bridge** button that cannot succeed.

## Root cause

`MachineManager.setup()`'s catch put every failed bridge update of a
registered PC (other than a `MachineActionRequired`) into `state:
"error"`, `action_required: "update_bridge"`, including a failure that
came from SSH not reaching the PC at all. Nothing re-probes an `error`
state, so the prompt stayed until the user acted.

The likely path: after an app update raises `REMOTE_BUNDLE_VERSION`, the
reconnect finds the old bridge and queues the automatic update. That
update opens a new SSH connection. If the MacBook has gone to sleep or
been switched off by then (or drops out mid-update), the connect fails
and the PC is parked on the update prompt for good.

## Fix

- An update job that could not reach a registered PC no longer parks it.
It goes back on the normal reconnect backoff (`retryLater`, factored out
of `lost()`), shows **Reconnecting…** with ssh's error, and has
`action_required: null`. When the PC answers, `reconnect()` re-runs the
version check, so `update_bridge` (and the automatic update) comes back
only when the bridge really is out of date.
- "Could not reach" means one of two things. SSH never came up and ssh's
error is not a refusal (`Permission denied`, host-key failures). Or SSH
came up and the master connection then died (new
`SshConnection.connected()`, read before `disconnect` closes it).
- Behaviour that stays the same: a PC that refuses the saved key keeps
**Update bridge** / **Sign in and update…**, as the `updateBridge`
comment requires. An update that fails on a PC that still answers
(download, install) also keeps the button and its reason. The job's own
`action_required`, which the dialog reads, is unchanged.
- No new UI strings. CHANGELOG (Unreleased → Fixed) and
`docs/remote-pcs.md` are updated.

## Tests

`server/machines.test.ts` has four new cases on a registered PC with SSH
faked:
- connect timeout → `reconnecting`, no action
- connection dropped mid-update → `reconnecting`, no action
- `Permission denied (publickey)` → keeps `update_bridge`
- a failure on a PC that still answers → keeps `update_bridge`

The first two fail without the fix. `bun run check fast` passes.

## Second path: a cancelled update (182bdd8)

On the user's live server, the MacBook record read `state:
"disconnected"`, `action_required: "update_bridge"`, with the error
`mux_client_request_session: read from master failed: Broken pipe …
Connection timed out`. That record has two steps behind it:
1. An earlier update failed after the SSH master died. The old catch
stored that error, `update_bridge` and `state: "error"`. The first
commit now turns this into reconnecting.
2. A later update job was **cancelled** (Cancel update, a closed dialog,
or the 600 s `job.timer`). The catch calls `disconnect()`, which sets
`"disconnected"`, then skips everything else for a cancelled phase. The
stale `action_required`/`error` stay, and no retry is scheduled, so the
PC stays on the prompt for good.

The new test `reconnects after a cancelled update, and asks again only
once the PC answers` reproduces that exact record without the fix.

The fix: a cancelled update of a registered, enabled PC (owning the
runtime, server not stopping) clears both fields and goes back on the
reconnect backoff. When the PC answers with an old bridge, it shows the
**Update bridge** button. The cancelled automatic update does not
restart by itself (`updateCancelled`) until the PC connects or the user
starts an update.

**Restarts and long outages:** `machines.json` stores no state, error or
action, so a restart starts every PC `disconnected` and reconnects it.
While a PC stays off, `reconnect()` fails at `ssh.start` and `lost()`
keeps it reconnecting. `action_required` is set in only two places: when
`prepare`/`verify` throw `MachineActionRequired` (SSH up and the PC
answering), or in the setup catch for a refusal or a failure with the
master still alive (the PC answered).

## Not covered / not verified

- Not tried against a real Mac that goes offline; the SSH failures are
faked at `SshConnection`.
- A PC whose update failed while it was still reachable, and which was
turned off afterwards, still shows the update prompt, because nothing
re-probes an `error` state. A periodic SSH liveness probe could cover
that case. It would change how the prompt behaves, so I left it for a
separate decision.
- Telling a refusal apart from an unreachable PC before SSH comes up
relies on ssh's stderr wording (OpenSSH's standard messages).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(omo): read no DONE for a turn that ended on a model error (#715)

## What was reproduced

An OmO pane's status comes from its session file
(`server/omo-status.ts`). An assistant answer with `stopReason: "error"`
— what OmO writes when the provider gives up after its retries or a
request times out — ended the turn exactly like a normal `stop`.
`CompletionTracker.settle()` then saw "idle after work" and latched the
pane as `done`, so it read DONE and web push / the browser alerted "work
finished" while the agent was still alive.

The new test in `server/omo-status.test.ts` replays the issue's sequence
as OmO records it: work, an error answer ("…could not complete the
request after 5 retries…"), the stop-state record, a `model_change`
fallback, more work on the fallback model, a second error ("Request
timed out."). Before the fix it read `working, done, working, done`.

## What changed

- `omoTurnAfter` marks a rest that came from an `error` answer as
`failed`. The pane still reads READY (not RUN: nothing OmO writes tells
a retry from giving up, as before).
- `OmoStatus.poll` tells that rest as `rest: "failed"` on `onChange`;
`omoChanged` passes it to `CompletionTracker.observe(…, rest)`, which
drops the pane's "worked" mark and any DONE kept from an earlier turn:
READY, no DONE, no alert. Snapshots, resync and restarts then agree,
since nothing is latched.
- A later answer after a failed rest (a retry that got through, or a new
prompt answered within one poll) is told as `rest: "answered"`: the
tracker marks the work itself and the pane goes straight to DONE. No
synthetic RUN is sent first, since that would make the turn 0 ms long
and the default "long turn" alerts would skip it. With no turn start
seen, push and the browser treat the length as unknown and alert.
- The failed rest is kept with what the pane read when its session could
not be told for a while, so a retry that answered meanwhile is still a
finish.
- Kept conservative (a missed DONE rather than a made-up or repeated
one): the remembered failure belongs to its session file; only an answer
timestamped after the failure counts as its retry; the tracker accepts
it only while nothing (work, DONE) was told of the pane since the failed
rest; an error read from a file found anew (restart, `/resume`) is
`failed-before` and leaves a DONE kept from another turn alone.
- Known limit: a retry's DONE that never read RUN has an unknown length
to both alert paths, so the default "long turn" setting alerts it even
after a short retry. Carrying the real duration needs turn timing in the
pane-status frames; left for a follow-up.
- Other agents are untouched: the flag is only set by OmO's session
reader and `observe` defaults to `failed = false`.

Not changed: `stopReason: "aborted"` (user interrupt) still ends as
DONE; an OmO pane whose session file cannot be told keeps herdr's status
as before. Wrong DONEs already saved in `completions.json` clear on the
pane's next turn or focus.

## How it was verified

- New unit tests: the issue's sequence reads `working, idle, working,
idle`, READY in every snapshot after; a retry's answer then reads
`done`; a normal turn afterwards still reads `working, done`. A DONE
kept from an earlier turn is dropped by a failed rest, and a retry that
answered while the session could not be told is still DONE.
- `bun run typecheck`, `HERDR_TEST_MODE=unit bun test` for `completion`,
`omo-status`, `omo`, `omo-tasks`.
- `bun run check run bun test ./server/omo.contract.test.ts
./server/omo-prompt.contract.test.ts` on an isolated herdr: 10 pass.
- `bun run check fast`: all ok (2261 pass, 0 fail).
- Alerts: web push (`server/push.ts`) and the browser's alerts
(`shouldNotifyStatus` in `shared/notify-policy.ts`) fire only on
`done`/`blocked`, so a READY rest alerts nothing on either path.

Closes #687

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(chat): match Windows Codex store paths and refuse ambiguous Claude stores (#714)

## What was reproduced

No Windows PC here, so every case was reproduced with Win32 path
semantics (`path.win32`) and an in-memory SQLite on Linux:

- **#587 case 1 (prefixed `CODEX_HOME`)**:
`path.win32.relative("\\?\D:\codex\sessions",
"D:\codex\sessions\2026\x.jsonl")` returns the absolute file path, so
the containment check in `codexRolloutPath` rejected a rollout inside
the store whenever only one side kept `\\?\`. The reverse (plain root,
prefixed file) fails the same way.
- **#587 case 2 (case-different cwd)**: `cwd IN ('D:\work\app',
'\\?\D:\work\app')` returns no row for a stored `\\?\d:\work\app`.
- **#586**: with records of the same live process in `~/.claude` and
`~/.claude-copy`, `windowsProcessStore` returned `null`, the same as "no
record", and `claudeTranscriptPath` fell back to `~/.claude`.

## Already fixed on main (#518)

- Point 1 (rollout paths with `\\?\`) and the prefix spelling of point 2
(`cwd IN (?, ?)` with both spellings) were fixed in c04c760 (#582,
released in v0.4.1). Point 3 (picker resume) is #717.

## What changed

- `server/codex.ts`:
- `rolloutInsideStore(root, file, paths)` strips `\\?\` on **both**
sides before `relative()`.
- A Windows cwd is one directory under the `\\?\` prefix and either
drive-letter case, and is otherwise compared exactly.
- `storedCwdCondition(cwd)` gives the three thread queries a binary `cwd
IN (…)` over those spellings, so the queries stay on Codex's `(archived,
cwd, …)` index.
- `sameDirectory` / `codexPeersIn` apply the same rule to the peer
checks (`claimedByOtherPanes`, `resumedElsewhere`, the short-thread
guard), so the peer checks and the queries agree.
  - POSIX cwds are exact as before.
- `server/claude-store.ts`:
- `windowsProcessStore` throws `AmbiguousClaudeStore` when more than one
store holds the process's live PID record. Like a miss, it is not
cached; "no record" still returns `null` and keeps the default-store
fallback.
- Stores are told apart by `claudeStoreKey`: volume + file id where
there is one, else the path without `\\?\` and without case. So
`CLAUDE_CONFIG_DIR=\\?\C:\…\.claude`, a junction or a link to
`~/.claude` is one store, not an ambiguous pair.
- `server/conversation.ts` (one hunk): `AmbiguousClaudeStore` becomes
`ConversationUnavailable("no_session_id")` instead of `~/.claude`.

### Scope narrowed after review

A cwd stored in another letter case **past the drive letter**
(`\\?\d:\Work\app` for `D:\work\app`) is deliberately **not** matched:
the chat stays unavailable.

- A Windows directory can be case-sensitive (`app` and `App` are then
two directories), and only the file system can tell.
- The identity-based version took a stat per spelling, a discovery scan
with a limit and a cache. Two adversarial review passes showed each of
those could fail open: another directory's or another pane's
conversation.
- Fail-closed was chosen instead. #587 therefore stays open for that
case.

## How it was verified

- Unit cases, each failing before its fix:
- `server/codex.test.ts`: a prefixed root or rollout counts as inside
the store, with traversal, another drive and UNC cases.
- `server/codex.test.ts`: the drive-letter and prefix spellings are
found; a case-sensitive sibling with 33 newer threads neither joins nor
crowds out the 32; POSIX case stays exact; the query plan is a SEARCH on
`(archived=? AND cwd=?)`; peers follow the same rule.
- `server/claude-store.test.ts`: two stores that both claim the process
reject with `AmbiguousClaudeStore`; a `CLAUDE_CONFIG_DIR` link to
`~/.claude` is one store; `claudeStoreKey` drops `\\?\` and case without
a file id.
- `bun run typecheck`, `HERDR_TEST_MODE=unit bun test
./server/codex.test.ts ./server/claude-store.test.ts
./server/codex-processes.test.ts ./server/conversation.test.ts
./server/windows-native.test.ts`, `bun run check fast` (all ok).
- Not run on a Windows PC.

## Issues

Closes #586
Refs #587: case 1 and the drive-letter part of case 2 are fixed; a cwd
differing in case past the drive letter stays unavailable by design (see
above).
Refs #518: points 1–2 fixed in #582, point 3 in #717.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(server): use pane.get for single-pane lookups (#724)

## Change

Fix audit finding H6 by replacing whole-session snapshots used only to
inspect one pane.

- Add a typed `paneGet(paneId, socketPath?)` wrapper over `pane.get`
with `{ pane_id }`, returning the generated `PaneInfo`; reuse it for
scroll metadata.
- Use it for pane context, chat input checks, collapsed Codex queue
checks, image uploads, push title lookup and on-demand Claude
background-task discovery.
- Keep the previous optional-pane behavior in input checks. Herdr 0.9.3
returns `pane_not_found` with the existing message for unknown panes, so
the same HTTP helpers and envelopes continue to apply.
- Add unknown-pane HTTP contract cases for commands and files; the
existing image unknown-pane contract also passes.

`terminalInfoFor` still needs snapshot layout rects. Conversation
readers still need peers to disambiguate Devin, Codex and OmO transcript
identities, so `server/conversation.ts` remains unchanged. Global
collector, completion normalization, agent name allocation and cache
recovery retain their snapshots.

No HTTP/WS shapes, endpoints, browser source, generated types or agent
settings changed.

## Validation

All checks ran in
`/home/devswha/workspace/herdr-web-ui-wt/refactor-pane-get-lookups`.

- PASS: `bun install`.
- PASS: `bun run check fast` - workflow syntax, generated types,
typecheck and build passed; 2,285 unit tests passed, 7 existing
platform/fixture skips, 0 failures.
- PASS: `bun run check run bun test --timeout 15000
./server/api.contract.test.ts ./server/submit.contract.test.ts
./server/pending-input.contract.test.ts ./server/herdr/client.test.ts` -
180 tests passed, 0 failures.
- PASS: `bun run check run bun test --timeout 15000
./server/codex.contract.test.ts ./server/omp.contract.test.ts
./server/opencode.contract.test.ts
./server/claude-session.contract.test.ts` - 19 tests passed, 0 failures.
- PASS: `git diff --check`.

The source-state verification reports and full logs are preserved under:

- `/tmp/parity-qa-H6/fast/report.json`
- `/tmp/parity-qa-H6/contracts/report.json`
- `/tmp/parity-qa-H6/conversation-contracts/report.json`

### Isolated real-surface smoke

Built client served on port 7457 with a temporary app state directory
and isolated Herdr session `herdr-web-ui-qa-H6`.

- Opened the real chat pane in Playwright/Chromium at 1280 x 800 and 390
x 844; screenshots visually inspected and phone page had no horizontal
overflow.
- Sent a command from web chat while Herdr's TUI was concurrently
attached in a PTY; exact output appeared in the TUI and socket output
event.
- Typed a command through the Herdr TUI; the same output appeared in web
chat.
- `curl -sS -i -X POST http://127.0.0.1:7457/api/pane/image -H
'Content-Type: application/json' --data
'{"pane_id":"w1:p1","content_type":"image/png","data_base64":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="}'`
returned HTTP 200 and a project-local PNG path.
- No Playwright page errors.

Screenshots:

- `/tmp/parity-qa-H6/desktop-chat.png`
- `/tmp/parity-qa-H6/phone-chat.png`
- `/tmp/parity-qa-H6/phone-reverse.png`

Supporting evidence:

- `/tmp/parity-qa-H6/image-curl.txt`
- `/tmp/parity-qa-H6/tui-web-to-herdr.txt`
- `/tmp/parity-qa-H6/tui-herdr-to-web.txt`
- `/tmp/parity-qa-H6/output-events.json`
- `/tmp/parity-qa-H6/pane-list.json`
- `/tmp/parity-qa-H6/session-snapshot.json`
- `/tmp/parity-qa-H6/pane-get-schema.json`
- `/tmp/parity-qa-H6/qa-report.md`

Herdr RPCs used: `pane.get` for converted metadata reads; retained
`session.snapshot` for layout/global/peer needs. QA used
`workspace.create`, `pane.read`, `pane.send_text`/`pane.send_keys`,
`agent.prompt` with the existing shell fallback, `events.subscribe` with
exact output matches, plus CLI `pane list` and `api snapshot`.

### Cleanup and limits

Browsers, web service, both owned TUI processes, QA Herdr session and
persistent service monitors stopped. `server stop` exited 0; the
temporary QA root and uploads were removed. Evidence under
`/tmp/parity-qa-H6/` was retained. Successful check runners cleaned
their isolated sessions.

No browser source changed, so no UI regression suite was required beyond
the requested smoke. The large `server/index.ts` LSP diagnostic request
timed out, but full TypeScript checking passed. Optional gate-reviewer
spawning was unavailable at the task nesting limit; the implementer
reviewed the full diff. No product failures or open behavior issues
found.

* feat(settings): show herdr's agent integrations read-only (#722)

## What changed

- **`GET /api/integrations`** wraps herdr's `integration.list` RPC
(herdr 0.9.3) via `integrationList()` in `server/herdr/client.ts`, one
connection per call. It answers `{ integrations: AgentIntegration[] }`
in herdr's order; any other method is `400 method_not_allowed`.
`AgentIntegration` / `IntegrationsResponse` are hand-written in
`shared/protocol.ts` (the saved schema predates the RPC; `state` is
widened with `(string & {})`), with the route catalogued there.
- **Remote PCs:** it is a plain GET like `/api/agents`, so it joins the
bridge path list and `MACHINE_PROXY_PATH` (`machines.test.ts` asserts
it). The client is `fetchIntegrations(machineId)` in `src/lib/api.ts`,
bound in `useMachineApi()`. The Settings dialog renders outside a
`MachineContext`, so the page shows the local PC; a remote PC's list is
reachable through the proxy but no UI picks a PC yet.
- **Settings → Agent integrations** (new page, `Plug` icon, before
About) built from `SettingsGroup`/`SettingsRow`: agents found on the
PC's PATH first, then a "Not found on this PC" group. Installed rows
show a check. Missing or outdated rows show `herdr integration install
<label>` (the label is the CLI name, e.g. `antigravity-cli`) with a Copy
button using `copyText` and the select-the-text fallback.
- **Read-only by design:** nothing calls
`integration.install`/`integration.uninstall`, and there is no
install/uninstall button; installing writes into an agent's own
hooks/settings, which stays the user's call in a terminal.
- i18n ko/ja/zh, demo answer in `site/demo/transport.ts` (one of each
state), the page added to `scripts/settings-pages-demo-regression.ts`,
CHANGELOG line under Unreleased → Added.

## How it was checked

- `bun run check fast` → PASS on 81becc7 (workflow syntax, generated
types, typecheck, build, unit tests: 2285 pass, 0 fail)
- `bun run check run bun test --timeout 15000
./server/api.contract.test.ts` → PASS (99 pass, 0 fail) (new case:
`/api/integrations` equals `integration.list` from the isolated herdr;
`claude` present; states in the known set; POST refused)
- `bun scripts/settings-pages-demo-regression.ts` → PASS (every page
incl. Agent integrations fits 390px/320px in en/ko/ja/zh; Back/history
cases pass)
- Manual QA, isolated session `herdr-web-ui-qa-D` with
`HOME`/`XDG_CONFIG_HOME` in a temp dir, web server on :7455, Playwright
Chromium at 1280px and 390px:
- before: 17 rows, all "Not installed", 0 install/uninstall buttons,
Copy put `herdr integration install pi` on the clipboard, no page
errors, no sideways scroll; only `GET /api/integrations` requests.
- acted in herdr: Enter in herdr's own TUI integrations tab (TUI client
attached to the same session) installed claude + codex; `herdr
integration install opencode` from the CLI; codex's temp hook marked v1
to get `outdated`.
- after reopening the page: claude/opencode "Installed" with a check,
codex "Installed, but older than this herdr" + its command, matching
`herdr integration status` (`claude: current (v10)`, `codex: outdated
(v1 < v8)`, `opencode: current (v13)`).
- Screenshots (local): `/tmp/parity-qa-D/before-desktop.png`,
`before-phone.png`, `after-desktop.png`, `after-phone.png`;
TUI/CLI/snapshot evidence beside them.

## herdr RPCs used

`integration.list` only.

## Known limits

- herdr's `integration.list` omits Letta (experimental in `herdr
integration status`), so the page does too.
- The page reads once per opening; it does not poll. Reopen it after
installing.
- The Settings page shows the local PC only (see Remote PCs above).



Remote PCs: an SSH PC whose bridge predates this endpoint shows a plain
"not offered yet" message; it gets the page with the next remote runtime
release (no bundle is published here).

* fix(tabs): preserve numeric custom names (#723)

## Change

A tab label is automatic only when empty or equal to its current place,
so a numeric custom label survives closing an earlier tab. Adds unit
coverage and an Unreleased Fixed entry.

## Validation

- bun run check fast — PASS (workflow syntax, generated types,
typecheck, build, unit suite).
- bun test ./src/lib/tabName.test.ts — PASS (5 tests).
- Isolated herdr session herdr-web-ui-qa-H5, 1280px Chromium capture:
/tmp/parity-qa-H5/tab-strip-desktop.png shows label 3 after the earlier
tab closed.
- herdr api schema --json confirms tab fields include label and stable
number, without a custom-label marker. Repro used tab.create, tab.list,
tab.close, and pane.split; remaining state was w1:t2, number:2, label:3.

No known limitations.

* feat(palette): list and run herdr plugin actions (#720)

## What changed

The web UI can list and run the manifest actions of the herdr plugins
installed on a PC, as herdr's own TUI does.

- **Server**: `GET /api/plugins/actions` (`plugin.list` +
`plugin.action.list`, grouped by plugin with its enabled state; actions
for another platform are left out, command lines stay on the server) and
`POST /api/plugin/action { plugin_id, action_id, pane_id? }`
(`plugin.action.invoke`). Wrappers in `server/herdr/client.ts`, shapes
and route catalogue in `shared/protocol.ts`, both routes added to the
remote-PC proxy allowlist and to the bridge-token paths, answers in
`site/demo/transport.ts`.
- **Palette**: a **Plugin actions** group runs an action against the
selected pane. The row shows "Running…" and the palette stays open until
the server answers; a failed command says why above the list
(`role="alert"`); a pane the action opened and focused is selected.
- **Settings → About**: a small read-only "herdr plugins" group (name,
version, actions, Enabled/Disabled). It is not drawn when no plugin is
installed.

## What herdr does, measured on 0.9.3

- `plugin.action.invoke` takes each `PluginInvocationContext` field as
given and fills a missing one from **its own focus**, not from the pane
named: `focused_pane_id` alone came back with the focused workspace's ID
and label, and an unknown pane ID was accepted. So the server validates
the pane against a snapshot and sends workspace, tab, labels, cwd, agent
and status itself.
- Invoke answers while the command is still `running`. The route polls
`plugin.log.list` for that log entry for up to 5 s, so the browser gets
`succeeded`, `failed` (exit code and the end of stderr) or `running`.
- **Plugin panes**: the invoke answer names no pane, but an action can
open one by calling `herdr plugin pane open` (this repository's own
`phone` action does). `opened_pane_id` is a pane that did not exist
before the invoke and that herdr focuses after it; the palette selects
it.
- `plugin.action.list` includes a disabled plugin's actions and other
platforms' actions; herdr refuses them with `plugin_disabled` /
`platform_unsupported`. The palette offers neither.

herdr RPCs used: `plugin.list`, `plugin.action.list`,
`plugin.action.invoke`, `plugin.log.list`, `session.snapshot`.

## How it was checked

| Command | Result |
|---|---|
| `bun run check fast` | PASS (2288 pass, 7 skip, 0 fail; typecheck
included) |
| `bun run check run bun test --timeout 15000
./server/plugins.contract.test.ts` | PASS (8 pass, 0 fail) |
| `bun run check run bun scripts/file-viewer-regression.ts` (drives the
palette over Settings and a preview) | PASS |
| Browser + TUI QA driver on an isolated herdr session (own
`XDG_CONFIG_HOME`), Chromium via playwright-core | PASS, 18 of 18 checks
|

The contract test links a plugin from a temp directory into the run's
own herdr config and unlinks it afterwards. herdr keeps linked plugins
per user, not per session, so the suite skips itself when the herdr
under test uses the user's own `~/.config/herdr` (the shared
`herdr-web-ui-test` session); `bun run check` and CI give it a config
directory of its own.

QA evidence (local paths on the build PC, not committed):
`/tmp/parity-qa-C/`
- `01-desktop-palette-group.png`, `02-desktop-in-flight.png`,
`04-desktop-failed-action.png`, `05-desktop-opened-pane-selected.png`,
`06-desktop-settings-plugins.png` at 1280px
- `09-phone-palette-group.png`, `10-phone-in-flight.png`,
`11-phone-failed-action.png` at 390px
- Web → herdr: `marker-from-web.json` (the context the plugin command
received: the web's selected pane, `invocation_source: herdr-web-ui`),
`tui-02-after-web-marker.txt` and
`socket-02-pane-list-after-web-marker.json` (the pane the action
renamed), `tui-03-after-web-open-board.txt` and
`socket-03-snapshot-after-web-open-board.json` (the plugin pane,
focused)
- herdr → web: `herdr-side-invoke.json` +
`07-desktop-after-herdr-side-invoke.png` (a plugin pane opened from
herdr's CLI appears in the web),
`08-desktop-plugin-disabled-in-herdr.png` (a plugin disabled in herdr
leaves the palette)
- `qa-results.txt`, `qa-driver.ts`, `qa-plugin.toml`

## Known limits

- Where an action's pane opens is the plugin's choice: `herdr plugin
pane open` uses herdr's focus unless the command passes `--target-pane`
or `--workspace`. The context is correct either way (`HERDR_PANE_ID` and
the context JSON name the web's pane).
- A popup-placement plugin pane has no pane ID and shows only in a herdr
TUI client; the web cannot select it.
- An action whose only context is `selection` is not offered: the web
has no selected text to hand over. Without a selected pane only `global`
(or context-less) actions are offered.
- A command still running after 5 s is reported as `running` and the
palette closes; its later failure is only in `herdr plugin log list`.
- Installing, enabling, disabling and removing plugins stay with herdr.
A remote PC on an older bridge answers 404 and the group is simply
absent there.
- Remote PCs get the routes with the next `remote-vN` bundle; none was
built here.

* fix(collector): refresh sessions on herdr lifecycle events (#725)

## Change

Fix audit H4: structural changes made in herdr's TUI now invalidate the
web roster/layout through the existing `session-changed` frame instead
of waiting for five-second polls.

- Subscribe to workspace, worktree, tab, pane and layout lifecycle
invalidations.
- Coalesce event bursts in the existing 500 ms window; snapshot retry
timers cannot delay browser invalidation.
- Reconcile authoritative pane IDs, closing and reopening the complete
per-pane status subscription after moves.
- Retain browser, machine and collector polls and recovery/health
checks. No UI or wire-shape changes.

## Validation

- PASS `bun install` (Bun 1.4.2).
- PASS `bun run check fast`: workflow syntax, generated types,
typecheck, build and unit suite, exit 0.
- PASS `bun run check run bun test --timeout 15000
./server/collector.contract.test.ts`: 3 tests, 0 failures; socket
`workspace.rename`, `tab.rename`, `pane.zoom` each produced WS
`session-changed` within 591-601 ms. Isolated herdr 0.9.3.
- PASS `bun run check run --build bun scripts/ui-regression.ts`: built
real client and existing desktop/mobile regression suite, exit 0. Only
lock refusals were retried, in bounded 30-second intervals; no test
failure was retried.
- PASS concurrent real browser and TUI on isolated `herdr-web-ui-qa-H4`,
bridge port 7456, temporary state: web tab rename appeared in TUI and
socket snapshot; TUI workspace rename painted in 703 ms; TUI tab rename
push arrived in 546 ms and painted in 650 ms.
- APPROVE read-only Codex `gpt-6.1-sol`, `xhigh`, fast-tier review under
an 8 GB memory cap: no must-fix findings
(`/tmp/parity-qa-H4/review.txt`).

## Evidence

Local evidence root: `/tmp/parity-qa-H4/`.

Screenshots: `desktop-web-to-tui.png`,
`desktop-tui-workspace-rename.png`, `desktop-tui-tab-rename.png`
(1280x800), `phone-tui-tab-rename.png`, `phone-tui-workspace-rename.png`
(390x844).

Timings/state/receipts: `timings.json`, `snapshot.json`,
`pane-list.json`, `tui-screen.txt`, `tui.typescript`, `qa-report.md`,
`cleanup.txt`, `fast-report.json`, `contract-report.json`,
`browser-report.json`.

QA setup RPCs: `workspace.create` and `tab.create` with `focus:false`,
`workspace.focus`, `session.snapshot`. Reverse actions used actual TUI
Ctrl+B, Shift+W/Shift+T rename dialogs, not API substitutes.

## Limits and cleanup

Screenshots are local paths, not hosted attachments. The first QA bridge
startup used an unset process environment and briefly reached the
default socket; attach was refused as `attach_held`, without takeover.
It was stopped; all retained captures came from a fresh owned browser
context against the explicitly isolated socket.

The browser, port-7456 bridge, TUI and QA herdr were stopped; temporary
QA state was removed. Both check runners cleaned their owned sessions.
No merge, main push or tag. Remote bridge deployment and GitHub CI are
not claimed by these local checks.

* feat(terminal): add native pane history search (#729)

## Change

Add **Find in terminal** to the header's More menu and Mod+Shift+F. The
find bar searches
herdr's stored pane history, shows the native match ordinal/count and
supports next/previous,
Enter, Shift+Enter and Escape. Keyboard focus remains in Find across
Chat transitions and
deferred upload completion; find text is never delivered to the
terminal.

`POST /api/pane/find` owns the HTTP contract in `shared/protocol.ts`,
validates requests and
uses fresh herdr RPC connections. Remote-PC routing is allowlisted and
machine-bound. Add
ko/ja/zh translations, explicit demo unavailability, a changelog entry
and native/browser
regression coverage. xterm scrollback remains zero; native attach
redraws display the match.

## Validation

- `bun install` — PASS.
- `bun run check fast` — PASS: workflow syntax, generated-type
freshness, typecheck, build
  and unit tests. Report:

`node_modules/.cache/check/fb6199c3-9334-4764-b95e-97dbf943080b/report.json`.
- `bun run check run bun test --timeout 15000
./server/find.contract.test.ts` — PASS,
8 tests: found, next/previous, not found, punctuation, malformed query,
unknown pane,
navigation validation and a real native revision race with no stale
scroll. Report:

`node_modules/.cache/check/73624152-7932-4eb7-a90c-8f068b246879/report.json`.
- `bun run check run --build bun scripts/ui-regression.ts` — PASS,
including the new
find regression, real keyboard focus, Chat-to-Find, touch-menu access
and a delayed real
  image upload. Report:

`node_modules/.cache/check/bef52677-5a76-4903-9af4-9973b4c51b86/report.json`.
- `UI_EVIDENCE_DIR=/tmp/parity-qa-G bun run check run --build bun
scripts/pane-find-regression.ts`
  — PASS as a focused check before the full suite.
- Codex `gpt-6.1-sol`, xhigh, fast tier — final APPROVE after focus/race
findings were
fixed and regression-tested.
`/tmp/parity-qa-G/codex-review-upload-followup.txt`.

Final manual QA used owned `herdr-web-ui-qa-G`, port 7454 and a
temporary app state directory.
Print `FIND_QA_G_UNIQUE` followed by 320 lines: GUI search finds 1 of 1
and displays it.
A concurrently attached herdr TUI shows the same match; typing
`TUI_REVERSE_QA_G` there
updates the web terminal. Desktop 1280px and phone 390px captures in
both themes:

- `/tmp/parity-qa-G/desktop-find.png`
- `/tmp/parity-qa-G/desktop-find-light.png`
- `/tmp/parity-qa-G/phone-find.png`
- `/tmp/parity-qa-G/phone-find-light.png`

RPC/TUI evidence: `/tmp/parity-qa-G/reviewed-evidence.json`,
`reviewed-tui-found.txt`,
`reviewed-tui-reverse.txt`, `native-pane-list.txt`,
`native-snapshot.json`, `history-read.json`
and `search-schema.json`. Cleanup receipts:
`/tmp/parity-qa-G/cleanup-receipts.json`.

## Native API and limits

Use `pane.get`, `pane.copy_motion`, `pane.copy_search` and
`pane.scroll`. Copy mode ne…
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