fix(edit-history): remote reads get time for their size, follow CLAUDE_CONFIG_DIR, keep the conversation after /clear

Review findings on the remote edit history:

- A forward read may carry 13 MiB and "Load older" 29 MiB; the flat 30 s
  read timeout cut those off below ~1 MiB/s. The timeout now adds the
  transfer time at 256 KiB/s (remoteEditSourcesTimeoutMs). It ends a hung
  read, it does not cut a slow one.
- The transcript lookup looked only under $HOME/.claude. It now uses an
  absolute CLAUDE_CONFIG_DIR of the ssh command's environment, else
  $HOME/.claude, for both the projects and the live sessions/<pid>.json.
  A value set only in .zshrc stays invisible (the lookup is a plain sh);
  documented, and the panel's remote no-transcript note says so instead
  of promising the next prompt (a remote host sends no hooks).
- After a remote /clear claudeSessionId stays the launch id, so once the
  remote claude exited the lookup fell back to the cleared conversation
  and the panel flipped back. The sync now hands the lookup the stored
  conversation, tried right after the live one (remoteTranscriptIds).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
RandalixandClaude Opus 5.5 committed 2026-11-05 11:19:52 +01:00
1 parent c4ba14235f
commit 0a6a877415
10 files changed
+169 -36

No files matched your search

+1 -1
View File
@@ -768,7 +768,7 @@ Matching semantics: a query containing `/` matches the relative path, otherwise
Paths, agent descriptions and diff lines render via `textContent` only (browser test with a hostile path). The header button uses the marker-class pattern (`btn-edit-history--hidden`) and stays off phones (`mobile.css`, locked in `test/mobile-header-buttons-policy.test.ts`). Docker and non-claude sessions answer `unsupported` and are never read.
⚠️ **Remote (SSH) sessions: read over ssh, never woken.** The sync stays path-based; its deps get the session, and `server.ts` routes them to the local reader or to `remote-files.ts`. `transcriptPath` is one ssh round trip (`buildRemoteClaudeTranscriptLocateCommand`): first the conversation the claude process in the session's remote pane writes now (its `~/.claude/sessions/<pid>.json`, found through the pane's process tree, since a remote host sends Codeman no hooks), else `<claudeSessionId>` then `<id>`, each the newest `$HOME/.claude/projects/*/<id>.jsonl`; the result is an absolute path on the host. A read pass is one more round trip (`buildRemoteClaudeEditSourcesCommand`) with the local reader's semantics: offset clamped to the size, a block with `len` ends at `offset + len`, a new file reads its tail, one budget shared main-first, `onlyRequested` for "Load older", at most 8 KiB of `meta.json` per subagent (within a 4 MiB meta budget). `test/remote-edit-sources.test.ts` runs both scripts on a real `sh` against a temp `$HOME` and compares the read byte for byte with `readClaudeEditSources` on seeded random transcripts, so `/clear` catch-up, "Load older", cursors and the store LRU behave remotely as locally. Every request value reaches the script only after validation (id `[A-Za-z0-9._-]`, relPath `main` or `subagents/agent-<id>.jsonl`, integer offsets) and shellescaped; the scripts run under `sh -c` with `LC_ALL=C`. Only the host's default `$HOME/.claude` is searched. ⚠️ **Never wakes:** the reads go through `buildRemoteFileCommand` + `runWithRemoteSshLimit` (`ConnectTimeout=10`), so a sleeping host is a connect timeout and the panel shows `unavailable` with the stored entries; the edit-history read path reaches neither `remote-wake.ts` nor `session-routes.ts`, even transitively (guard in `test/remote-edit-sources.test.ts`). Without hooks there is no `transcript:tool_end`, so an open panel re-reads on `session:idle` and the refresh button only.
⚠️ **Remote (SSH) sessions: read over ssh, never woken.** The sync stays path-based; its deps get the session, and `server.ts` routes them to the local reader or to `remote-files.ts`. `transcriptPath` is one ssh round trip (`buildRemoteClaudeTranscriptLocateCommand`): first the conversation the claude process in the session's remote pane writes now (its `<config dir>/sessions/<pid>.json`, found through the pane's process tree, since a remote host sends Codeman no hooks), else the stored history's conversation, then `<claudeSessionId>`, then `<id>` (`remoteTranscriptIds`: nothing moves `claudeSessionId` after a remote `/clear`, so tried first it would flip the panel back to the cleared conversation once the remote claude exits), each the newest `<config dir>/projects/*/<id>.jsonl`; the result is an absolute path on the host. A read pass is one more round trip (`buildRemoteClaudeEditSourcesCommand`) with the local reader's semantics: offset clamped to the size, a block with `len` ends at `offset + len`, a new file reads its tail, one budget shared main-first, `onlyRequested` for "Load older", at most 8 KiB of `meta.json` per subagent (within a 4 MiB meta budget). `test/remote-edit-sources.test.ts` runs both scripts on a real `sh` against a temp `$HOME` and compares the read byte for byte with `readClaudeEditSources` on seeded random transcripts, so `/clear` catch-up, "Load older", cursors and the store LRU behave remotely as locally. Every request value reaches the script only after validation (id `[A-Za-z0-9._-]`, relPath `main` or `subagents/agent-<id>.jsonl`, integer offsets) and shellescaped; the scripts run under `sh -c` with `LC_ALL=C`. The config dir is an absolute `CLAUDE_CONFIG_DIR` of the ssh command's environment, else `$HOME/.claude` (claude's own rule); the remote claude runs under `$SHELL -i -l -c`, the lookup under a plain `sh`, so a value set only in `.zshrc` is not seen. A read's timeout grows with its byte cap (`remoteEditSourcesTimeoutMs`: 30 s plus the transfer at 256 KiB/s), since a forward pass may carry 13 MiB and "Load older" 29 MiB. ⚠️ **Never wakes:** the reads go through `buildRemoteFileCommand` + `runWithRemoteSshLimit` (`ConnectTimeout=10`), so a sleeping host is a connect timeout and the panel shows `unavailable` with the stored entries; the edit-history read path reaches neither `remote-wake.ts` nor `session-routes.ts`, even transitively (guard in `test/remote-edit-sources.test.ts`). Without hooks there is no `transcript:tool_end`, so an open panel re-reads on `session:idle` and the refresh button only.
### Config backups
+3 -2
View File
@@ -324,8 +324,9 @@ How it reads:
directory. A `CLAUDE_CONFIG_DIR` set outside Codeman (in a shell profile or a wrapper script)
is only known after the next prompt; until then the window says "No transcript found for this
session yet". A remote session's transcript is looked up on its host over SSH: the
conversation its Claude writes now, else the session's own, under the remote user's
`~/.claude` (a `CLAUDE_CONFIG_DIR` on the remote host is not followed).
conversation its Claude writes now, else the one the window showed last, else the session's
own, under the remote user's `~/.claude`, or under `CLAUDE_CONFIG_DIR` when the host sets it
for every SSH command (`.zshenv`, `/etc/environment`); one set only in `.zshrc` is not seen.
- **The history is stored, and it holds secrets.** Every diff an agent wrote is copied to
`~/.codeman/edit-history/<session-id>.json`, including API keys or `.env` contents it edited. The file is
readable by your user only, capped at 500 edits and 4 MiB of diff text per session (the oldest