Skip to content

Session continuity via sessionId chaining#11

Merged
wesm merged 9 commits intomainfrom
session-continuity
Feb 23, 2026
Merged

Session continuity via sessionId chaining#11
wesm merged 9 commits intomainfrom
session-continuity

Conversation

@wesm
Copy link
Copy Markdown
Owner

@wesm wesm commented Feb 23, 2026

Summary

  • Group continuation sessions with their originals via parent_session_id chaining, extracted from Claude JSONL sessionId fields
  • Frontend walks parent_session_id chains to find root sessions and groups all sessions sharing the same root
  • Prune command excludes sessions that have children to avoid breaking continuity chains
  • Old databases (missing parent_session_id column) are dropped and rebuilt from scratch on startup

How it works

Claude JSONL files have a sessionId field on user/assistant records. For original sessions, sessionId matches the file's UUID. For continuations, it carries the parent file's UUID, forming a linked list (A -> B -> C). The parser extracts this and stores it as parent_session_id. The frontend walks the chain to find the root and groups all sessions sharing the same root.

Test plan

  • go vet ./... && go test -tags fts5 ./... -- all pass
  • cd frontend && npx vitest run -- all 318 tests pass
  • Manual: delete DB, restart agentsview, verify continuation sessions group under original with correct first message

wesm and others added 7 commits February 22, 2026 17:59
Extract the slug field from Claude Code JSONL records and store it
per-session in the database. Sessions sharing the same (project, slug)
are grouped in the sidebar as a single row with a continuation badge
(e.g., "x3"), and clicking selects the most recent session.

Changes across the stack:
- Parser: extract first non-empty slug from any JSONL record
- Database: add slug column with project+slug index, wire through
  all Session column lists and UpsertSession
- Sync: pass parsed slug through to database
- Frontend: add SessionGroup type with grouping logic, update
  SessionList virtual list to iterate groups, add continuation
  badge to SessionItem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Select primary session by ended_at (most recent) instead of
  started_at, consistent with sidebar sort order
- Mark sidebar row active when activeSessionId matches any session
  in the group, not just the primary
- Handle slug index creation error instead of silently ignoring it

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Primary session in a group is now selected using
ended_at ?? started_at ?? created_at, matching the backend's
COALESCE ordering. Handles null ended_at (in-progress sessions)
and equal ended_at ties deterministically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The virtualizer preserved the previous session's scroll offset via
initialOffset and postUpdate. When measureCacheKey changes (session
switch), reset initialOffset to 0 and set scrollTop = 0 on the DOM
element instead of restoring the stale position.

Also use COALESCE-style recency key (ended_at ?? started_at ??
created_at) for primary session selection, matching backend ordering.
Added tests for null ended_at, null both, and equal ended_at ties.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The slug-based grouping didn't work because original sessions have
slug: null -- only continuations carry a slug, so the original session
was never grouped with its continuations.

Replace with sessionId-based chaining: extract sessionId from the first
user/assistant record in Claude JSONL files. If sessionId differs from
the file's own UUID, store it as parent_session_id. The frontend walks
parent_session_id chains to find the root and groups all sessions
sharing the same root.

Migration clears file_hash for existing Claude sessions missing
parent_session_id so they get re-parsed on next sync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The hash-clearing migration ran on every startup because original
sessions always have parent_session_id IS NULL, forcing a full
re-parse of all Claude sessions each time.

Remove the migration entirely -- normal sync populates
parent_session_id for fresh databases. Existing users should
recreate their database.

Also exclude sessions that have children from prune candidates
to avoid breaking continuity chains.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Detect missing parent_session_id column on startup and delete the
database file so it gets recreated from scratch. Session data is
re-synced from source files on the next sync cycle.

Move parent_session_id and its index into schema.sql and remove
the incremental ensureColumn migrations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link
Copy Markdown

roborev-ci bot commented Feb 23, 2026

roborev: Combined Review (6c109ce)

Verdict: Changes are not yet safe to merge due to one High and two Medium issues that can cause data loss or incorrect session grouping behavior.

High

  1. Unintended DB deletion on transient schema-check errors
    File: internal/db/db.go:100, internal/db/db.go:109
    needsRebuild() treats any sql.Open/query error as rebuild-needed, and Open() then calls dropDatabase(). Temporary failures (lock contention, I/O hiccups, permission changes, etc.) can trigger destructive DB deletion and data loss/local DoS.
    Recommended fix: Make needsRebuild return (bool, error) and rebuild only on positively confirmed schema mismatch (e.g., parent_session_id missing). Propagate other errors and abort init without deleting files.

Medium

  1. Rebuild path ignores file deletion failures
    File: internal/db/db.go:122, internal/db/db.go:124
    dropDatabase() ignores os.Remove errors for DB/WAL/SHM files. If deletion partially fails, startup can proceed with inconsistent on-disk state and later fail in hard-to-diagnose ways.
    Recommended fix: Return/handle removal errors (except os.IsNotExist) and fail fast with a clear error.

  2. Timestamp ordering uses lexical string comparison
    File: frontend/src/lib/stores/sessions.svelte.ts:243, frontend/src/lib/stores/sessions.svelte.ts:257, frontend/src/lib/stores/sessions.svelte.ts:338
    minString/maxString and recencyKey rely on raw string ordering. Mixed ISO variants (e.g., with vs without fractional seconds) can sort incorrectly and produce wrong primarySessionId or group bounds.
    Recommended fix: Compare normalized numeric timestamps (Date.parse/epoch) or enforce a fixed-width canonical timestamp format before comparison.


Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

wesm and others added 2 commits February 22, 2026 22:11
The test was creating an old schema and racing two concurrent Opens.
With the drop-and-rebuild approach, both callers race to delete the
file, causing file-not-found errors. Update the test to use a
current schema since the interesting concurrent behavior is now in
init(), not incremental migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
needsRebuild now returns false on any transient error (can't open,
query failure) instead of treating it as a schema mismatch. Only
deletes the database when the schema check positively confirms the
parent_session_id column is missing.

dropDatabase now returns an error if file removal fails (ignoring
ENOENT), failing fast instead of proceeding with inconsistent state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link
Copy Markdown

roborev-ci bot commented Feb 23, 2026

roborev: Combined Review (e0bb649)

Verdict: Changes are not clean; 1 High and 3 Medium issues need follow-up before merge.

High

  1. Destructive DB rebuild can delete healthy or unintended files (DoS risk)
    Refs: internal/db/db.go:88, internal/db/db.go:97, internal/db/db.go:104, internal/db/db.go:114, internal/db/db.go:121, internal/db/db.go:124
    Open() can trigger dropDatabase() when needsRebuild() returns true on broad probe failures (including transient open/query/lock issues), which can delete a valid DB under contention. If DB path input is not tightly constrained, this also creates arbitrary file deletion risk. There is also a race window between rebuild check and deletion.
    Suggested fix:
    • Rebuild only on explicit, confirmed legacy-schema condition (not generic probe errors).
    • Treat transient probe failures as errors, not rebuild signals.
    • Enforce canonicalized allowlisted DB path root.
    • Serialize rebuild with an inter-process lock.
    • Prefer quarantine/atomic rename over immediate delete.

Medium

  1. Untrusted parent_session_id can bypass prune/retention logic
    Refs: internal/parser/claude.go:82, internal/db/sessions.go:357, internal/db/sessions.go:533
    parent_session_id from untrusted JSONL is persisted and later used by prune protection (NOT EXISTS child.parent_session_id = sessions.id), allowing crafted links to keep unrelated sessions from pruning (storage retention bypass / DoS).
    Suggested fix: validate format, enforce existence/scope integrity (FK or verified same-scope parent), and only honor trusted links in prune logic.

  2. dropDatabase() ignores file removal failures
    Ref: internal/db/db.go:121
    Ignoring os.Remove errors can leave partial cleanup/stale files and cause confusing follow-on failures.
    Suggested fix: surface cleanup failures and fail Open() with actionable error details when rebuild cleanup is incomplete.

  3. Concurrency migration test now masks rebuild-race regressions
    Refs: internal/db/db_test.go:1447, internal/db/db_test.go:1502
    Updated race test accepts "no such file" during concurrent Open(), which may hide real destructive race behavior introduced by drop/recreate migration strategy.
    Suggested fix: add targeted concurrent old-schema rebuild tests and assert no destructive startup failures.


Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

@roborev-ci
Copy link
Copy Markdown

roborev-ci bot commented Feb 23, 2026

roborev: Combined Review (9ca16f8)

Verdict: Changes are functionally solid overall, but there are 3 Medium-severity issues that should be addressed before merge.

Medium

  1. Unsafe rebuild delete flow can remove unintended files and race under concurrency
    Files: internal/db/db.go:91, internal/db/db.go:123, internal/db/db.go:140
    Issue: Schema-triggered rebuild uses os.Remove(path + suffix) without strong path safety checks and without serialized check/delete/open, creating path traversal/symlink/race risk if path is influenced by config/input and availability risk under concurrent access.
    Suggested fix: Canonicalize/enforce DB path under an app-owned directory, reject symlinks/non-regular files via Lstat, and serialize rebuild with an inter-process lock or atomic rename/recreate flow.

  2. Parent session detection can lock onto the wrong ID
    File: internal/parser/claude.go:85
    Issue: Parent detection stops at the first non-empty sessionId, even if it matches the current sessionID; this can miss the actual parent appearing later in the file.
    Suggested fix: Keep scanning until finding a sessionId that differs from sessionID (or EOF).

  3. Rebuild decision path can miss recovery on probe errors
    Files: internal/db/db.go:91, internal/db/db.go:103, internal/db/db.go:177
    Issue: If needsRebuild() hits a transient probe error and returns false, openAndInit() continues and may fail on legacy schema assumptions, causing startup failure for that run without automatic recovery.
    Suggested fix: Add a guarded fallback: on init failure with schema-mismatch signatures (e.g., missing parent_session_id), perform one rebuild and retry open/init once.


Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

@wesm wesm merged commit 07f572a into main Feb 23, 2026
6 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.

1 participant