You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
/export only exports the loaded portion of a long thread #50014
In a long thread, /export writes only the part of the conversation that's currently
loaded. The file starts partway in and everything before that is missing. There is no
warning: the export succeeds and the file looks complete.
Scrolling to the top first, so the whole thread loads, gives a correct export. I'd
expect /export to write the full thread regardless of scroll position.
Plugins
No response
OpenCode version
v2.0.10
Steps to reproduce
Open a thread with well over 100 messages, so older messages aren't loaded.
Run /export without scrolling, taking the default options.
Check the first message in the file. It begins partway into the session.
Scroll to the top of the same session, then run /export again.
This file has the full thread. Scroll position is the only difference.
The report is exact, and the repository already contains the control case for it: the same dialog has a second format that does not have the bug.
Why the markdown file starts partway in — the markdown export is built in the TUI from the loaded window, while the JSON export is built on the server from storage.
So the markdown formatter can only ever see the messages the client has currently loaded, and the loaded window is exactly what scrolling changes — which is why step 5 of your repro produces the full file. The JSON format of the same dialog returns the whole session, because transfer.export reads the messages from storage with no window at all. Same session, same dialog, different data source: a free A/B check for anyone verifying the fix.
Two surfaces, not one. The sibling command at :1105 (Copy session transcript) calls the same formatSessionTranscript(sessionData, messages(), true), so copying from a long thread silently copies the same partial window.
Fix options
Point the markdown path at the complete history as well — page the rest in (or use the full-items view) before formatting, keeping the formatter where it is.
Move the markdown rendering server-side and expose a format next to the existing sanitize on session.export, so both formats are built from transfer.export's complete message list. This removes the second, divergent copy of the transcript logic and makes "complete by construction" true for every format.
My default is (2), because the server-side source is already complete and the endpoint already has the query-parameter shape for it; (1) is the smaller diff if you'd rather not grow the export API. Happy to implement either.
One related question: should an export that cannot see the whole session fail or warn instead of writing a file that looks complete? That is the part of the report that costs users data, and it is independent of which fix above you prefer — I would make the markdown path refuse to write when the client cannot prove it has the full range, unless you'd rather keep it best-effort.
Happy to have the issue assigned if either direction looks right.
Activity
argszero commentedon Sep 19, 2026
I'd like to work on this.
The report is exact, and the repository already contains the control case for it: the same dialog has a second format that does not have the bug.
Why the markdown file starts partway in — the markdown export is built in the TUI from the loaded window, while the JSON export is built on the server from storage.
/exportcommand, both formatspackages/tui/src/routes/session/index.tsx:1113(id: "session.export",slash: { name: "export" }):1132formatSessionTranscript(sessionData, messages(), options.thinking, options.tools):1134client.api.session.export({ sessionID, sanitize })messages()is:160const messages = () => data.session.message.list(route.sessionID)— the same list the timeline pages into:3608formatSessionTranscript(session, messages, thinking, tools)— a pure formatter over the array it is handed; it cannot pagepackages/server/src/handlers/session.ts:166→SessionTransfer.export→packages/core/src/session/transfer.tsreadssessions.messages({ sessionID, order: "asc" })So the markdown formatter can only ever see the messages the client has currently loaded, and the loaded window is exactly what scrolling changes — which is why step 5 of your repro produces the full file. The JSON format of the same dialog returns the whole session, because
transfer.exportreads the messages from storage with no window at all. Same session, same dialog, different data source: a free A/B check for anyone verifying the fix.Two surfaces, not one. The sibling command at
:1105(Copy session transcript) calls the sameformatSessionTranscript(sessionData, messages(), true), so copying from a long thread silently copies the same partial window.Fix options
formatnext to the existingsanitizeonsession.export, so both formats are built fromtransfer.export's complete message list. This removes the second, divergent copy of the transcript logic and makes "complete by construction" true for every format.My default is (2), because the server-side source is already complete and the endpoint already has the query-parameter shape for it; (1) is the smaller diff if you'd rather not grow the export API. Happy to implement either.
One related question: should an export that cannot see the whole session fail or warn instead of writing a file that looks complete? That is the part of the report that costs users data, and it is independent of which fix above you prefer — I would make the markdown path refuse to write when the client cannot prove it has the full range, unless you'd rather keep it best-effort.
Happy to have the issue assigned if either direction looks right.
Tarquinen commentedon Oct 20, 2026
This was fixed by #50733. That PR merged into the
v2branch, so GitHub didn't close this issue automatically. Closing as completed.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊