[luv-297] feat: multi-harness session log download#298
Conversation
Generalises `app/api/download/[project]/[session]/route.ts` from a Claude-only file streamer to a per-CLI dispatcher in `lib/download-session.ts`, and drops the `!isExternal` gate on the session-page Download Logs button. The five external file-backed CLIs (Codex, Copilot, Cursor, Pi, Gemini) already had per-CLI session loaders that discover an on-disk JSONL via `find*Transcript(sessionId)`; the dispatcher streams those bytes verbatim so replay tooling sees the original CLI shape. OpenCode keeps sessions in SQLite (~/.local/share/opencode/opencode.db) across three tables (session/message/part), so a flat JSONL would lose the part rows. New `getOpenCodeSessionExport(sessionId)` returns the full SQLite snapshot and the dispatcher emits it as `application/json` with the JSON `data` columns parsed for readability — preserving the underlying structure rather than the viewer's collapsed-to-message shape. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…g export Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR introduces a per-CLI Download Logs feature for the dashboard. It adds a dispatcher ( ChangesDownload Logs Feature
Sequence DiagramsequenceDiagram
participant Client as Browser / Client
participant Page as app/project/[name]/<br>session/[sessionId]/page.tsx
participant Route as app/api/download/<br>[project]/[session]/route.ts
participant Dispatcher as lib/download-<br>session.ts
participant Export as lib/opencode-<br>sessions.ts
participant FS as File System /<br>SQLite DB
Client->>Page: Load session page
Page->>Client: Render Download Logs button<br>(with cli param)
Client->>Route: GET /api/download/proj/ses?cli=opencode
Route->>Dispatcher: isKnownCli('opencode')?
Dispatcher-->>Route: true
Route->>Dispatcher: resolveDownloadSource('opencode', 'proj', 'ses')
Dispatcher->>Export: getOpenCodeSessionExport('ses')
Export->>FS: Query SQLite for session/messages/parts
FS-->>Export: { session, messages[], parts[] }
Export-->>Dispatcher: OpenCodeSessionExportData
Dispatcher-->>Route: { kind: 'synthesized', body, contentType, extension }
Route->>Client: 200 + JSON body<br>Content-Type: application/json<br>Content-Disposition: attachment
Client->>Route: GET /api/download/proj/uuid?cli=codex
Route->>Dispatcher: resolveDownloadSource('codex', 'proj', 'uuid')
Dispatcher->>FS: Locate transcript file
FS-->>Dispatcher: /path/to/transcript.jsonl
Dispatcher-->>Route: { kind: 'file', path }
Route->>FS: Stream file
FS-->>Route: File content
Route->>Client: 200 + streamed JSONL<br>Content-Type: application/x-ndjson
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
#296) * [luv-298] fix: name the offending subcommand in block-work-on-main deny message The deny formatter ran a second, looser regex that captured the first git subcommand in chained commands, so a blocked `git checkout -b feat/x && git commit -m "y"` would render as "Git checkout on main is blocked" — misleading users into thinking the policy blocks branch creation. It does not. Reuse the existing GIT_COMMIT_MERGE_RE capture so the message always names the actual commit/merge/rebase/cherry-pick offender. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-298] docs: correct block-work-on-main description The English doc said the policy "denies checking out main or master branches directly" and listed `protectedBranches` as branch names that cannot be checked out — both wrong, and the same misconception that prompted the deny-message fix in the previous commit. The policy denies commit/merge/rebase/cherry-pick on protected branches, not checkout. Update the doc to reflect actual behavior. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-298] chore: correct PR number in changelog entry Original entry referenced #298 (the branch name); the actual PR is #296. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
__tests__/lib/opencode-sessions.test.ts (1)
176-210: ⚡ Quick winAdd one regression case for export query failure behavior.
Please add a test where the session query succeeds but message/part query fails, and assert
getOpenCodeSessionExport()returnsnull(to avoid silently treating DB errors as empty export data).✅ Suggested test case
describe("getOpenCodeSessionExport", () => { + it("returns null when a downstream export query fails after session lookup", async () => { + let callCount = 0; + mockExec.mockImplementation(() => { + callCount++; + if (callCount === 1) { + return JSON.stringify([ + { id: "ses_x", project_id: "p1", slug: null, directory: "/repo", title: "T", time_created: 1, time_updated: 2 }, + ]); + } + throw new Error("db locked"); + }); + await expect(getOpenCodeSessionExport("ses_x")).resolves.toBeNull(); + });As per coding guidelines,
**/*.ts: Always add unit tests in__tests__/for new behavior; never modify existing tests just to make them pass.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@__tests__/lib/opencode-sessions.test.ts` around lines 176 - 210, Add a regression test inside the describe("getOpenCodeSessionExport") suite that simulates a successful session query but a failing messages/parts query and asserts getOpenCodeSessionExport(...) returns null; use the existing test helpers (mockQueries and mockExec) to return a session row for the first query and make the second query throw or return an error-like response, then call getOpenCodeSessionExport("ses_x") and expect null to ensure DB errors for messages/parts are not treated as empty exports.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/opencode-sessions.ts`:
- Around line 263-268: The current code calls runOpenCodeDb for messages and
parts and uses "?? []" to convert null (query failure) into an empty array,
which hides DB errors and yields partial/empty exports; replace the "?? []"
behavior with explicit null-checks that throw or propagate an error including
the sessionId and which query failed (e.g., after running
runOpenCodeDb<OpenCodeMessageRow>(...) and runOpenCodeDb<OpenCodePartRow>(...),
if the result is null throw new Error(`Failed to load messages for session
${sessionId}`) and similarly for parts) so failures surface instead of silently
returning empty arrays.
---
Nitpick comments:
In `@__tests__/lib/opencode-sessions.test.ts`:
- Around line 176-210: Add a regression test inside the
describe("getOpenCodeSessionExport") suite that simulates a successful session
query but a failing messages/parts query and asserts
getOpenCodeSessionExport(...) returns null; use the existing test helpers
(mockQueries and mockExec) to return a session row for the first query and make
the second query throw or return an error-like response, then call
getOpenCodeSessionExport("ses_x") and expect null to ensure DB errors for
messages/parts are not treated as empty exports.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5b8256d6-5e5f-42cf-b7c0-9c04b4612ff6
📒 Files selected for processing (10)
CHANGELOG.md__tests__/api/download-route.test.ts__tests__/lib/download-session.test.ts__tests__/lib/opencode-sessions.test.tsapp/api/download/[project]/[session]/route.tsapp/project/[name]/session/[sessionId]/page.tsxdocs/architecture.mdxdocs/dashboard.mdxlib/download-session.tslib/opencode-sessions.ts
Per CodeRabbit on PR #298: `getOpenCodeSessionExport` was coalescing `runOpenCodeDb` failures (which return `null`) to `[]` via `?? []`, making a broken DB call indistinguishable from a genuinely empty session in the export. Honest 404 is better than a misleading 200 with `messages: []` / `parts: []`. New unit test pins the behaviour when the message/part queries error mid-flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Download Logsbutton on the session page was gated behind!isExternal, and the API route atapp/api/download/[project]/[session]/route.tsonly resolved Claude paths. Newlib/download-session.tsdispatches per-CLI through the existingfind*Transcript(sessionId)helpers and streams the original on-disk JSONL byte-for-byte for Codex / Copilot / Cursor / Pi / Gemini (in addition to Claude). The route validates?cli=<id>againstisKnownCli, validatessessionIdper-CLI shape (UUID for six,^ses_[A-Za-z0-9]+$for opencode), andlogActivitynow records the originating CLI.getOpenCodeSessionExport(sessionId)returns{session, messages, parts}with the JSONdatacolumns parsed; the route emits it asapplication/jsonwith a<sessionId>.jsonfilename. Malformed JSON indatasurvives as the raw string.__tests__/lib/download-session.test.ts(12 cases — happy path + 404 for all 7 CLIs, 400 on bad cli / malformed session id, real on-disk fixture for codex),__tests__/api/download-route.test.ts(6 route-level cases including the back-compat default ofcli=claude), and 4 new cases in__tests__/lib/opencode-sessions.test.tscoveringgetOpenCodeSessionExport.docs/dashboard.mdx(the user-facing description of the Download button) anddocs/architecture.mdx(the ascii-art directory tree comment for the route).Test plan
bun run lintclean (1 pre-existing warning unrelated to this change).bunx tsc --noEmitclean.bun run test:run— all 1511 unit tests across 70 files pass.bun run test:e2e— all 290 e2e tests across 13 files pass.bun run build— Next + dist build succeed.bun run dev:/api/download/__/<uuid>?cli=codexagainst a real~/.codex/sessions/.../<uuid>.jsonl— HTTP 200, byte-identical to source (174312 bytes diff'd)./api/download/<projectFolder>/<uuid>(no?cli) against a real Claude transcript — HTTP 200, byte-identical (back-compat)./api/download/__/<sessionId>?cli=opencodeagainst a real OpenCode session — HTTP 200,application/json,Content-Disposition: ...filename="<sessionId>.json", body parses to{session, messages, parts}with parseddatacolumns./api/download/__/garbage?cli=codex→ HTTP 400 (malformed sessionId)./api/download/__/<uuid>?cli=bogus→ HTTP 400 (unknown cli)./api/download/__/<uuid>?cli=codex(no transcript on disk) → HTTP 404.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes v0.0.10-beta.0
New Features
Bug Fixes
Documentation