RFD D46: Session-Scoped Active Workspace
- Status: Draft
- Category: Design
- Authors: Jean Mertz git@jeanmertz.com
- Date: 2026-06-01
- Extends: RFD 020
- Requires: RFD 031
Summary
Each terminal session can select an active workspace with jp w use, after which jp q runs against it from anywhere without --workspace, the same way RFD 020 gives each session an active conversation. This also decides which checkout a command acts on when RFD 031 maps one workspace ID to several git worktrees.
Motivation
RFD 031 makes every git worktree of a repository share one user-local store keyed by workspace ID, which fixes durability and the directory-collision crash. It leaves one question unanswered: when a workspace ID resolves to several checkouts on disk, which one does a command act on?
Today there is no good answer. --workspace=<id> is ambiguous across checkouts, and nothing targets a workspace from outside its directory at all, so running jp always means first cd-ing into the right tree. Do nothing and that stays true: worktree users keep navigating by hand, and the multi-checkout case has no defined behavior.
RFD 020 already solved the same shape of problem for conversations: each tab tracks its own active conversation, and the feature is widely relied on. Applying that model one level up gives each tab an active workspace, so jp q works from anywhere and the multi-checkout ambiguity becomes an explicit, per-session choice.
Design
Workflow
A fresh terminal has no active workspace, so jp behaves exactly as it does today: it operates on the workspace you are standing in.
To drive a workspace from anywhere, select one for the session:
$ cd ~/scratch
$ jp w use ?
? Select a workspace
> jp ~/Projects/jp.git/my-feature
jp ~/Projects/jp.git/main
dotfiles ~/.dotfiles
$ jp q "summarize the last commit" # runs in ~/Projects/jp.git/my-featureThe choice is scoped to this tab, exactly like the active conversation in RFD 020: another tab can select a different workspace, and the two never interfere. jp w show reports the current selection and jp w use --clear drops it.
The rest of this section describes how that resolution works.
Two-layer session model
- Session identity is reused unchanged from RFD 020 (workspace-independent:
$JP_SESSION,getsid(0)/ console HWND, per-pane terminal vars). - New global layer: session to active workspace (a concrete checkout root), stored in a user-global session store at
~/.local/share/jp/sessions/<session-key>.json, above any<id>. - Existing per-workspace layer is unchanged: session to active conversation at
<id>/sessions/<session-key>.json. - Composition for
jp qfrom anywhere: resolve session to active workspace (global), enter it (see Execution context below), resolve session to active conversation (per-workspace), run.
Startup ordering
Selecting a workspace by ID happens before a Workspace exists, which inverts today's startup: run_inner loads the workspace first, then resolves session identity. A dedicated bootstrap step in jp_cli owns the pre-workspace resolution:
- Resolve session identity (
session::resolve). - Inspect cwd for a workspace, if any.
- Read the user-global session store for this session's active workspace.
- Read the per-workspace roots registries under the user data directory.
- Choose a concrete checkout root (see Precedence).
- Only then construct
Workspace, load config, load the conversation index, and run the command.
User-data scanning and root selection stay in this jp_cli step. jp_workspace::Workspace keeps managing an already-selected workspace and gains no awareness of how the root was chosen.
Execution context: the workspace root is the working directory
When JP resolves a workspace via the session-active pick (the command was launched from outside any workspace), it operates as if launched from the workspace root: the selected root becomes the process working directory before config loading, MCP servers, plugins, and local tools run. Today these use inconsistent bases (config and MCP/plugin spawns inherit the process cwd; local tools and attachments use workspace.root()), so without this invariant a from-anywhere run would mix contexts.
When JP is launched from inside a workspace, the working directory is left unchanged, so a subdirectory's .jp.toml chain still loads as it does today.
Accepted trade-off: under a from-anywhere run, relative paths such as jp q --attach ./foo.txt and jp config set --cwd resolve against the workspace root, not the launch directory. Revisiting that is future work.
Roots registry (one workspace ID, many checkouts)
The single storage symlink is replaced by a roots registry that maps one workspace ID to its checkouts on disk. A shared read-modify-write file would have a lost-update race that can silently drop a checkout from the set, so the registry is instead a directory of per-root files, one per checkout, mirroring how sessions and locks already work:
~/.local/share/jp/workspace/<id>/roots/<root-key>.json<root-key>is a stable hash of the checkout's canonical path, so each checkout owns exactly one file and writes never contend.- Each run upserts only its own file, recording the canonical path and a
last_usedtimestamp. No file is read-modified-written by more than one checkout. - Liveness is derived, not stored: a root is live when its path still resolves to a workspace whose
.jp/.idequals<id>. A path that was deleted, or recreated as a different workspace, is not live, and its file is pruned during the existing cleanup pass. - These are plain files, so the Windows symlink-privilege requirement that the old
storagesymlink imposed does not arise.
// ~/.local/share/jp/workspace/<id>/roots/<root-key>.json
{ "path": "/Users/jean/Projects/jp.git/my-feature", "last_used": "2026-06-01T18:25:00Z" }The roots registry is workspace-scoped (under <id>/). The session store is user-global (under sessions/, mapping a session to its active workspace). These are deliberately separate, not one store doing two jobs.
The jp w command surface
jp w use ?: list known workspaces (<id>dirs), expand each through the roots registry to its live checkouts, pick one, record it as the session's active workspace.jp w use --clear: drop this session's active workspace, falling back to cwd resolution.jp w ls: list known workspaces and their checkouts, mirroringjp c ls.jp w show: show the session's active workspace, how it was resolved, and whether cwd is overriding it, mirroringjp c show.jp -w <id>: one live root, use it; many, picker (interactive) or an error listing the roots (non-interactive). Pure addressing; mirrorsjp qwith no active conversation.-wtargets a single command and does not change the session's active workspace.
Precedence and the cwd-vs-active conflict
Interactive ladder: explicit -w wins; else if a session-active workspace is set and cwd resolves a different workspace, prompt; else cwd wins when present; else use session-active; else picker.
The conflict prompt fires on any difference (different workspace ID or a different checkout of the same ID):
How to proceed? [c/C/a/A/q]
c - use current workspace
C - use current workspace and make it session-active
a - use active workspace
A - use active workspace and don't ask again in this session
q - quit without running commandA persists on the session record and pins the session to the active workspace. It is interactive-only state, cleared with jp w use --clear (see Session store and cleanup).
Non-interactive mode ignores the session-active workspace entirely. A non-interactive command runs from inside a workspace or with an explicit --workspace, and errors otherwise. jp w use is itself an error in non-interactive mode. This keeps scripts deterministic: they never depend on hidden per-session state.
Reprompt on a missing active workspace
- If the recorded root no longer exists (worktree removed), re-prompt among the remaining roots, mirroring how
session_active_conversationreturnsNoneand falls back to the picker when the active conversation is gone.
Session store and cleanup
The global session store maps a session to an active workspace root, which is longer-lived than RFD 020's per-workspace mapping (a conversation history), so cleanup splits by session source:
getsid/Hwnd: reuse RFD 020's process-liveness check. The mapping is removed when the originating process is confirmed dead.Env(including$JP_SESSION): process liveness is unknown, and RFD 020's "are the referenced conversations gone" fallback does not apply because the mapping points at a workspace, not conversations. The mapping is removed when its active root no longer resolves to a live workspace, the same liveness check the roots registry uses.- A pinned
Achoice has no process bound forEnvsources, so it persists until the root dies or the user runsjp w use --clear.
Drawbacks
- A new user-global session store and its cleanup pass. It reuses RFD 020's process-liveness check for
getsid/Hwndsources but needs a distinct rule forEnvsources (see Session store and cleanup). - Cold-start double prompt: a fresh session run from nowhere prompts for a workspace, then a conversation. Acceptable for v1; optimization deferred.
- The conflict prompt adds a decision point, though only for users who have set a session-active workspace.
Alternatives
- Always cwd-wins, no session-active workspace. Rejected: never lets you run
jpfrom outside a workspace directory, the core goal. - Always session-active-wins over cwd. Rejected: silently runs against the wrong place when you
cdelsewhere. - A single global active workspace, not per-session. Rejected: breaks parallel tabs, the property RFD 020 users rely on.
- Multi-target symlink for the back-pointer. Not a filesystem primitive; the registry file models one-to-many natively.
Non-Goals
- Git awareness. Consistent with RFD 031, JP does not inspect worktree topology.
- Attachment portability across checkouts. Continuing a conversation in a different checkout can break path-relative attachments. RFD 065's snapshot model captures attachment content at attach time and does not re-resolve it on later runs, which removes most of this risk; what remains (resolving a new
--attachagainst the current checkout) is bounded by the precedence ladder and conflict prompt above, which are a guardrail, not a guarantee. - Re-embedding the workspace ID in conversation IDs. That regression fix is a separate, smaller change. It is complementary (visible IDs make
jp wandjp -wdiscoverable) but not required for this design to function. - Cross-machine sync.
Risks and Open Questions
- A workspace ID with no live checkouts (every worktree removed) cannot be entered by
jp -w <id>orjp w use. The intended behavior is an error pointing the user at a checkout; confirm that is sufficient. - Root-key derivation must be stable across runs and collision-resistant for distinct canonical paths. A hash of the canonical path is the intended approach.
Implementation Plan
Phase 1: Roots registry and -w <id> resolution
Replace the storage symlink with the per-root registry directory and resolve -w <id> against it: one live root, use it; many, picker/error; none, error. Derive liveness via the same-ID check.
Depends on: RFD 031 Phase 1. Pure addressing; can be merged independently of the session layer.
Phase 2: Startup boundary and execution context
Move session resolution ahead of workspace construction and add the jp_cli bootstrap step that selects the root. Establish the root-as-working-directory invariant for from-anywhere runs.
Depends on: Phase 1.
Phase 3: User-global session store and jp w surface
Add the user-global session store and the jp w commands (use ?, use --clear, ls, show), plus jp q-from-anywhere resolution and the source-split cleanup rules.
Depends on: Phase 2.
Phase 4: Precedence ladder, conflict prompt, and reprompt
Implement the interactive ladder, the cwd-vs-active prompt, the persisted A silence, the non-interactive rule, and reprompt-on-missing-root.
Depends on: Phase 3.
References
- RFD 020: Parallel Conversations, the session identity and history model this RFD extends.
- RFD 031: Durable Conversation Storage with Workspace Projection, the shared user-local store and migration this RFD requires.
- RFD 065: Typed Resource Model for Attachments, whose snapshot-at-attach model bounds the attachment-portability concern noted in Non-Goals.