mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(story-mode): the reader cannot stall the server, and reads only real transcripts
From review round 7 (security): - Eviction past the per-transcript caps dropped entries one shift() at a time after the whole tail was parsed: quadratic and never yielding (a 2 s event-loop stall on a 70 MB long turn, 51 s from 300k entries). It is one splice, done at every 1 MB yield, so the peak stays near the caps; and a window that saw a turn start which was then trimmed away no longer widens to a bigger tail that would be trimmed the same. - A FIFO or a device in place of a transcript or a helper's .meta.json (stat said size 0, then open() waited for a writer) pinned a libuv worker per request; four of them hang every fs, dns and crypto call on the server. Files are now opened non-blocking, checked with the handle's own stat (a regular file only) and read through that handle. - The cache held 6 transcripts for everyone, so a seventh open view made every poll a full re-read, and a 304 was decided only after reading. An unchanged file is now a 304 from a stat alone, and the cache holds up to 24 transcripts within 30 million characters. - Story Mode is the first route to send a transcript's content to the browser, so the hook-reported path is used only when it really lies (symlinks resolved) in a Claude projects folder: the session's relocated CLAUDE_CONFIG_DIR (now also used for the lookup by id) or the server's. Anything else falls back to the lookup by conversation id. Tests move their transcripts into the (temp) projects folder and cover a path outside it, a symlink out of it, a FIFO transcript, a FIFO meta file and a 100,000-entry tail. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7aa7106cb4
commit
250a77d945
6 files changed
+258
-44
No files matched your search
@@ -1059,7 +1059,7 @@ What Story Mode reads (the opt-in header button, App Settings → Header buttons
|
||||
- `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/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, else `<conversation id>.jsonl` under the Claude projects directory. 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. A transcript that cannot be read is a `500` with `OPERATION_FAILED`.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user