fix(story-mode): the rest of the PR review: lookups, polling, frame budget, linear links

From review round 7 (PR review):

- With no hook path (normal after a server restart), the transcript was
  looked up by conversation id only; it now also tries the Codeman id and
  a recovered session's `restored-<fragment>` transcript, as
  last-response does, and keeps what it found briefly (a lookup reads
  every project folder; a miss is kept only 4 s).
- A session Story Mode cannot draw is not polled, and a page with nothing
  drawn yet is polled like a finished one (every 15 s), not every 4 s.
- Helper steps: one request per helper is drawn, the newest; a failed
  refetch never writes over steps already shown.
- The Director's cut toggle, a skin change and the fonts arriving redraw
  within the frame budget, not every panel in one task.
- A bare name under an `ls` header finds its folder in one pass over the
  lines; a search per link over the text before it was quadratic (about
  1.5 s for 700 links on agent output).
- A page of many questions still keeps to 12 panels: questions fold last.
- Opening Story Mode while it is open never starts a second runtime; the
  status announcer carries data-i18n-skip; the turn key in the ETag is
  clipped.
- The crafted-input timing test runs at sizes where the old code took 6
  to 29 s and the new one under 10 ms, with a 1.5 s bound.
- Docs: `older`, `origin` (`scheduled`), which route answers a 500;
  mobile.css's stale comment; an orphaned doc comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 14:44:37 +02:00
1 parent e7ce3b8e56
commit 42d1c75a19
10 files changed
+191 -41

No files matched your search

+3 -3
View File
@@ -1056,16 +1056,16 @@ What Story Mode reads (the opt-in header button, App Settings → Header buttons
### Routes
- `GET /api/sessions/:id/story[?turn=<id>]`: one turn as a page. `data` is `{ supported: true, turns, page }`: `turns` lists up to 60 turns, oldest first (`{ id, at, prompt, origin, complete, interrupted, superseded?, toolCalls }`), and `page` is the newest turn, or the one `turn=<id>` names, or `null` when there is none (an unknown id included). `older: true` means the transcript is longer than the part that was read, so older turns are not listed.
- `GET /api/sessions/:id/story[?turn=<id>]`: one turn as a page. `data` is `{ supported: true, turns, page }`: `turns` lists up to 60 turns, oldest first (`{ id, at, prompt, origin, complete, interrupted, superseded?, toolCalls }`), and `page` is the newest turn, or the one `turn=<id>` names, or `null` when there is none (an unknown id included). `older: true` means older turns exist that `turns` leaves out: before the part of the transcript that was read, or past the 60 listed.
- `GET /api/sessions/:id/story/helpers/:agentId`: one helper (an Agent call, `agentId` from its event's `helper.agentId`) drawn from its own transcript, `<conversation>/subagents/agent-<agentId>.jsonl` beside the session's. `data` is `{ supported: true, helper: { agentId, type?, description?, finished? }, page }`, the same page shape, or `page: null` while the helper has written nothing yet. `agentId` must match `[A-Za-z0-9_-]{1,64}` (`400` otherwise).
Both answer `{ supported: false, reason }` for a session they cannot draw: its CLI writes no Claude Code transcript (registry `capabilities.transcript` is not `claude-jsonl`), or it is remote or Docker, whose transcript is not on this host. Both are read-only. The session resolves like every session route (ownership via `findSessionOrFail`; another user's session is `404`), and the file read is the session's own transcript, never one the request names: the path the Claude hook reported when it really lies (symlinks resolved) in a Claude projects folder (the session's relocated `CLAUDE_CONFIG_DIR`, or the server's), else `<conversation id>.jsonl` under that folder. Only a regular file is read, through one non-blocking handle, so a FIFO or a device in its place fails at once. An unchanged file is a `304` decided from a stat, before anything is read. Only its tail is read (8 MB, widened in steps to 64 MB while that tail holds no prompt to start the turn); file contents, original files and images never leave the server. A transcript that cannot be read is a `500` with `OPERATION_FAILED`.
Both answer `{ supported: false, reason }` for a session they cannot draw: its CLI writes no Claude Code transcript (registry `capabilities.transcript` is not `claude-jsonl`), or it is remote or Docker, whose transcript is not on this host. Both are read-only. The session resolves like every session route (ownership via `findSessionOrFail`; another user's session is `404`), and the file read is the session's own transcript, never one the request names: the path the Claude hook reported when it really lies (symlinks resolved) in a Claude projects folder (the session's relocated `CLAUDE_CONFIG_DIR`, or the server's), else `<conversation id>.jsonl` under that folder. Only a regular file is read, through one non-blocking handle, so a FIFO or a device in its place fails at once. An unchanged file is a `304` decided from a stat, before anything is read. Only its tail is read (8 MB, widened in steps to 64 MB while that tail holds no prompt to start the turn); file contents, original files and images never leave the server. On the page route, a transcript that cannot be read is a `500` with `OPERATION_FAILED`; the helper route answers `page: null` for a helper transcript it cannot read (one not written yet, or gone).
Responses carry a weak `ETag` with `Cache-Control: no-cache`; a matching `If-None-Match` gets a `304` without the page being rebuilt, which keeps a live view's frequent refetches cheap.
### Turns: where they start and end
A turn is one prompt and everything Claude did for it. `origin` says who sent the prompt: `human`, `task` (a background job reporting back), `peer` (another session) or `system`.
A turn is one prompt and everything Claude did for it. `origin` says who sent the prompt: `human`, `task` (a background job reporting back), `peer` (another session), `scheduled` (a scheduled task's run) or `system`.
- **Start.** A user line with a prompt starts a turn. A prompt that arrives while Claude works (typed, a background job's report, a message from another session) is written by current Claude Code builds as a `queued_command` attachment, not a user line; it joins the running turn. A prompt written twice under one `promptId` (a typed `/compact`, then its command record) is one turn. A slash command is a prompt event with tool `Command`.
- **End.** At `turn_duration`; at the Stop hook's `stop_hook_summary` (unless it prevented the stop) in sessions that never write `turn_duration`; at an interrupt; for a local slash command (`/compact`, `/model`), at its own output (`<local-command-stdout>`, kept as the prompt event's `output`). A turn still open when the next prompt starts has ended too, and is marked `superseded`.