Skip to content

feat(web,hub): export session conversation#808

Merged
tiann merged 4 commits into
tiann:mainfrom
swear01:fix/issue-793-session-export
Jun 5, 2026
Merged

feat(web,hub): export session conversation#808
tiann merged 4 commits into
tiann:mainfrom
swear01:fix/issue-793-session-export

Conversation

@swear01
Copy link
Copy Markdown
Contributor

@swear01 swear01 commented Jun 5, 2026

Closes #793

Summary

  • Add HapiSessionExport v1 shared schema/types and a guarded GET /api/sessions/:id/export hub endpoint.
  • Export chronological visible messages with queued/hidden rows omitted and a 20,000-message hard cap returning 413.
  • Add web header export action with JSON/Markdown dialog, persisted format preference, AbortController support, and Blob download.
  • Generate Markdown from the same export payload via normalizeDecryptedMessage with concise tool summaries.

Test plan

  • bun typecheck
  • bun run --cwd hub test src/web/routes/sessions.test.ts src/sync/messageService.test.ts --bail=1
  • bun run --cwd web test src/lib/sessionExport/markdown.test.ts --run
  • bun run test:hub
  • bun run test:web
  • bun run test:shared
  • bun run --cwd cli test src/claude/claudeRemote.test.ts --run (rerun after unrelated CLI timeout flake)

Note: A later full bun typecheck && bun run test attempt was interrupted/ignored at user request after hitting an unrelated CLI timeout flake in src/claude/claudeRemote.seam.test.ts; this PR does not touch CLI code.

Manual verification

  • Started isolated source env from branch fix/issue-793-session-export on port 43006.
  • User confirmed export behavior looked good.
  • Isolated env stopped before PR creation.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Export can reorder invoked scheduled messages — getAllMessages() reads by insertion seq, but scheduled local messages are inserted when queued and get invokedAt only when released/consumed. Once such a message is invoked, this export includes it at its old insertion position, while the visible thread pagination orders by COALESCE(invoked_at, created_at), seq; JSON/Markdown exports can therefore disagree with the visible conversation chronology. Evidence hub/src/sync/messageService.ts:99, related context hub/src/store/messages.ts:221.
    Suggested fix:
    const messages = this.store.messages.getAllMessages(sessionId)
        .filter(isExportVisibleStoredMessage)
        .sort((a, b) => {
            const aAt = a.invokedAt ?? a.createdAt
            const bAt = b.invokedAt ?? b.createdAt
            return aAt !== bAt ? aAt - bAt : a.seq - b.seq
        })
        .map(toDecryptedMessage)

Summary

  • Review mode: initial
  • One issue found: export ordering can diverge from the visible chat for scheduled messages after they are invoked.

Testing

  • Not run (automation). Add a hub test where a scheduled message is inserted first, a normal message is inserted second, then the scheduled message is marked invoked later; export should order by invoked/display time.

HAPI Bot

session: Session,
limit: number = SESSION_EXPORT_MESSAGE_LIMIT
): HapiSessionExportResult {
const messages = this.store.messages.getAllMessages(sessionId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] getAllMessages() returns rows by insertion seq, but scheduled local messages are inserted when queued and receive invokedAt later. After release/consume, this export includes them at their old insertion position, unlike the visible thread which orders by COALESCE(invoked_at, created_at), seq. That makes JSON/Markdown exports disagree with the visible chronology.

Suggested fix:

const messages = this.store.messages.getAllMessages(sessionId)
    .filter(isExportVisibleStoredMessage)
    .sort((a, b) => {
        const aAt = a.invokedAt ?? a.createdAt
        const bAt = b.invokedAt ?? b.createdAt
        return aAt !== bAt ? aAt - bAt : a.seq - b.seq
    })
    .map(toDecryptedMessage)

…essages

Export now uses COALESCE(invoked_at, created_at) ordering so JSON/Markdown
exports match the visible chat chronology after scheduled messages are invoked.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Minor] Markdown front matter can be broken by session metadata containing newlines — escapeYamlString() only escapes backslashes and quotes, but metadata.name, metadata.path, and metadata.host are user/session-controlled strings that can contain \n/\r. When those values are interpolated into quoted YAML scalars, a newline can terminate the field and inject extra front matter keys or produce invalid Markdown metadata. Evidence web/src/lib/sessionExport/markdown.ts:18.
    Suggested fix:
    function escapeYamlString(value: string): string {
        return value
            .replace(/\\/g, '\\\\')
            .replace(/"/g, '\\"')
            .replace(/\r/g, '\\r')
            .replace(/\n/g, '\\n')
    }

Summary

  • Review mode: follow-up after new commits
  • One minor issue found in Markdown export front matter escaping. Prior bot finding about scheduled-message export ordering appears addressed by the new sort and regression test.

Testing

  • Not run (automation): bun is not installed in this runner (/bin/bash: bun: command not found). Add a markdown export test with a session title containing a newline and quote.

HAPI Bot

return payload.session.id.slice(0, 8)
}

function escapeYamlString(value: string): string {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] Markdown front matter can be broken by session metadata containing newlines. escapeYamlString() only escapes backslashes and quotes, but metadata.name, metadata.path, and metadata.host are user/session-controlled strings that can contain \n/\r; interpolating those into quoted YAML scalars can terminate the field and inject extra front matter keys or produce invalid metadata.

Suggested fix:

function escapeYamlString(value: string): string {
    return value
        .replace(/\\/g, '\\\\')
        .replace(/"/g, '\\"')
        .replace(/\r/g, '\\r')
        .replace(/\n/g, '\\n')
}

Prevent session metadata containing newlines or quotes from breaking
Markdown export front matter.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings
None.

Questions
None.

Summary
Review mode: follow-up after new commits
No issues found in the latest full diff. The previous front-matter escaping finding is addressed in current head with CR/LF escaping and a regression test. Residual risk: download dialog behavior, AbortController cancellation, and persisted format preference are source-reviewed/unit-reviewed only; no browser-level export flow test found in repo/docs.

Testing
Not run (automation): bun is not installed in this runner (/bin/bash: bun: command not found).

HAPI Bot

@tiann tiann merged commit f086949 into tiann:main Jun 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(web,hub): export session conversation as JSON or Markdown download

2 participants