Skip to content

feat(code): session forking#2322

Draft
Basit-Balogun10 wants to merge 2 commits into
PostHog:mainfrom
Basit-Balogun10:posthog-code/session-forking
Draft

feat(code): session forking#2322
Basit-Balogun10 wants to merge 2 commits into
PostHog:mainfrom
Basit-Balogun10:posthog-code/session-forking

Conversation

@Basit-Balogun10
Copy link
Copy Markdown

@Basit-Balogun10 Basit-Balogun10 commented May 23, 2026

Summary

Closes: #330

  • Adds a Fork session from here button (GitBranch icon) on hover over any user message
  • Clicking it creates an independent task with its own git worktree checked out at the historical commit and the conversation pre-loaded up to that point
  • A Forked from: [title] banner at the top of the fork links back to the parent session

What changed

Agent

  • jsonl-hydration.ts — adds filterEntriesUpToMessage to slice a log up to message N, plus helpers to convert turns back to JSONL entries for seeding a new SDK session

Main process

  • ForkService — orchestrates the full fork pipeline: fetch S3 log → slice at message index → extract git HEAD SHA from checkpoint → create worktree → write JSONL → seed local cache
  • WorkspaceService.createWorkspaceFromFork — creates a new worktree at a specific commit SHA
  • AgentService — emits _posthog/agent_checkpoint (HEAD SHA) after each completed prompt turn so forks can check out the exact historical state
  • fork-relationship-repository.ts + migration 0006_fork_relationships.sql — SQLite table storing lineage (parent task ID, title, run ID, message index) so the banner survives parent task deletion
  • fork.ts tRPC router — exposes prepareFork mutation and getForkRelationship query

Renderer

  • useForkSession — hook that creates a new task + run via PostHog API, calls fork.prepareFork, then navigates to the new task
  • UserMessage — fork button wired up via onFork prop
  • ConversationView — passes onFork with the message index to each UserMessage
  • ForkedFromBanner — shown at the top of forked sessions with a clickable link back to the parent
  • SessionView — renders ForkedFromBanner when a fork relationship exists

Bug fixes included

Three bugs that caused the forked task to open with an empty conversation:

  1. writeLocalCache copied source session entries verbatim — parseLogContent extracted the wrong (source) sdkSessionId, pointing the agent at the wrong JSONL file. Fixed by appending a synthetic _posthog/fork_session_meta entry encoding the fork's own session ID.
  2. The task passed to navigateToTask had no latest_run, so the session service created a third task run instead of reconnecting to the pre-seeded one. Fixed by attaching latest_run to the navigated task.
  3. Even with latest_run.id set, log_url is null for a brand-new run, sending the session service down the createNewLocalSession path. Fixed by checking for existing local cache before falling through.

Reviewer notes

  • The checkpoint SHA is emitted per-turn so the worktree is checked out at the exact commit after that turn's agent work — not the current HEAD
  • For sessions created before this change (no checkpoint entries), ForkService falls back to the current HEAD of the source worktree
  • Cloud-mode tasks (workspace.worktreePath = null) hit the early-exit toast in useForkSession — the fork button is visible but non-functional; a follow-up can hide it for cloud tasks
  • Requires a full app restart after first deploy so the 0006_fork_relationships migration runs

Test plan

  • Hover over a user message — fork button appears in the hover toolbar
  • Fork from message 1 — new task navigated to, conversation shows only message 1 + response
  • "Forked from: [title]" banner visible; clicking it navigates back to parent
  • New worktree exists at ~/.posthog-code/worktrees/; git log HEAD matches state after turn 1
  • Fork from message 2 — new task has two messages, worktree at turn-2 commit
  • Old session (no checkpoints) — fork falls back to current HEAD, conversation still truncated correctly

Basit-Balogun10 and others added 2 commits May 22, 2026 13:37
Allows forking a task at any past message, creating an independent task
with its own git worktree checked out at the historical commit and the
conversation pre-loaded up to that point.

- Emit `_posthog/agent_checkpoint` (HEAD SHA) after each prompt turn
- filterEntriesUpToMessage helper slices S3 log to message N
- WorkspaceService.createWorkspaceFromFork creates worktree at a commit
- ForkService orchestrates the full fork pipeline
- fork_relationships SQLite table stores lineage (title kept if parent deleted)
- ForkedFromBanner shown at top of forked session with parent link
- Fork button (GitBranch icon) on hover over each user message

Generated-By: PostHog Code
Task-Id: d612e6ba-d4b7-4e35-a0fc-27bf370a0db1
Three bugs prevented the forked session from loading its pre-seeded
conversation:

1. writeLocalCache appended the source session's entries verbatim, so
   parseLogContent extracted the SOURCE session's sdkSessionId — pointing
   the agent at the wrong JSONL file. Fix: append a synthetic
   _posthog/fork_session_meta entry with the fork's own sdkSessionId.

2. The new task navigated to had no latest_run, so the session service's
   else-branch called createNewLocalSession — creating a third task run
   and ignoring the pre-seeded cache entirely. Fix: pass latest_run on
   the task object in useForkSession.

3. Even with latest_run.id set, log_url is null for a brand-new run, so
   the session service still hit the else-branch. Fix: before falling
   through to createNewLocalSession, check for an existing local cache;
   if present, reconnect to that run instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

Session forking

1 participant