fix(state): inherit cwd/git_repo_root on parent_session_id children

_insert_session_row never copied cwd/git_repo_root from a parent row when
parent_session_id was set, and git_repo_root wasn't even in the INSERT's
column list. The compression-fork path (and delegate/subagent spawns,
branch continuations) creates a child session without passing cwd/
git_repo_root at all, so the child's tip is born NULL — and since the
Desktop project sidebar groups sessions by cwd, the whole project silently
drops out of the sidebar every time a long conversation compresses. A
lineage that compresses repeatedly compounds this across generations.

Add git_repo_root to _insert_session_row's INSERT/COALESCE-on-conflict
column set, and backfill both cwd and git_repo_root from the immediate
parent row (single non-recursive hop, matching the existing COALESCE
"never overwrite an explicit value" contract) inside the same write
transaction whenever parent_session_id is set. A multi-generation chain
resolves correctly because each generation's own create_session call
already backfills from its (already-resolved) immediate parent.

Fixes #64709
This commit is contained in:
Simplicio, Wesley (ext) 2026-07-15 00:22:39 -03:00 committed by Teknium
parent b6a2b701c1
commit 3c74c12554
2 changed files with 70 additions and 3 deletions

View file

@ -2709,6 +2709,7 @@ class SessionDB:
parent_session_id: str = None,
cwd: str = None,
profile_name: str = None,
git_repo_root: str = None,
) -> None:
"""Insert a session row, enriching NULL metadata on conflict.
@ -2727,14 +2728,25 @@ class SessionDB:
a persisted, now-inactive row belongs to the caller's chat/thread before
switching to it (IDOR scoping without them the ``sessions`` table has
no chat/thread to compare).
When ``parent_session_id`` is set (compression fork, delegate/subagent
spawn, branch continuation) and this row's own ``cwd``/``git_repo_root``
are still NULL after the insert, they are backfilled from the parent
row. Callers of ``create_session`` for a child session historically
didn't propagate these fields themselves (e.g. the compression-fork
path), so a lineage could silently lose its working directory and drop
out of the project sidebar every time it forked (#64709). This only
fills NULLs an explicit ``cwd``/``git_repo_root`` on the child is
never overwritten.
"""
def _do(conn):
conn.execute(
"""INSERT INTO sessions (
id, source, user_id, session_key, chat_id, chat_type, thread_id,
model, model_config, system_prompt, parent_session_id, cwd, profile_name, started_at
model, model_config, system_prompt, parent_session_id, cwd,
profile_name, git_repo_root, started_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
model = COALESCE(sessions.model, excluded.model),
model_config = COALESCE(sessions.model_config, excluded.model_config),
@ -2745,7 +2757,8 @@ class SessionDB:
thread_id = COALESCE(sessions.thread_id, excluded.thread_id),
parent_session_id = COALESCE(sessions.parent_session_id, excluded.parent_session_id),
cwd = COALESCE(sessions.cwd, excluded.cwd),
profile_name = COALESCE(sessions.profile_name, excluded.profile_name)""",
profile_name = COALESCE(sessions.profile_name, excluded.profile_name),
git_repo_root = COALESCE(sessions.git_repo_root, excluded.git_repo_root)""",
(
session_id,
source,
@ -2760,9 +2773,22 @@ class SessionDB:
parent_session_id,
cwd,
profile_name,
git_repo_root,
time.time(),
),
)
if parent_session_id:
conn.execute(
"""UPDATE sessions
SET cwd = COALESCE(sessions.cwd,
(SELECT p.cwd FROM sessions p
WHERE p.id = sessions.parent_session_id)),
git_repo_root = COALESCE(sessions.git_repo_root,
(SELECT p.git_repo_root FROM sessions p
WHERE p.id = sessions.parent_session_id))
WHERE id = ? AND parent_session_id IS NOT NULL""",
(session_id,),
)
self._execute_write(_do)
def create_session(self, session_id: str, source: str, **kwargs) -> str:

View file

@ -139,6 +139,47 @@ class TestSessionLifecycle:
assert session["cwd"] == "/work/repo"
assert session["git_branch"] == "pets-feature"
def test_child_session_inherits_cwd_and_git_repo_root_from_parent(self, db):
"""A parent_session_id child born without cwd/git_repo_root (e.g. the
compression-fork path) must inherit both from its parent, so it
doesn't silently drop out of the project sidebar (#64709)."""
db.create_session(session_id="parent", source="cli")
db.update_session_cwd("parent", "/work/repo", git_repo_root="/work/repo")
db.create_session(session_id="child", source="cli", parent_session_id="parent")
child = db.get_session("child")
assert child["cwd"] == "/work/repo"
assert child["git_repo_root"] == "/work/repo"
def test_child_session_explicit_cwd_is_not_overwritten_by_parent(self, db):
"""A child that explicitly sets its own cwd/git_repo_root keeps it —
parent inheritance only fills in NULLs, never clobbers."""
db.create_session(session_id="parent", source="cli")
db.update_session_cwd("parent", "/work/repo-a", git_repo_root="/work/repo-a")
db.create_session(
session_id="child", source="cli", parent_session_id="parent",
cwd="/work/repo-b", git_repo_root="/work/repo-b",
)
child = db.get_session("child")
assert child["cwd"] == "/work/repo-b"
assert child["git_repo_root"] == "/work/repo-b"
def test_multi_generation_lineage_inherits_cwd(self, db):
"""cwd/git_repo_root propagate through a multi-hop compression chain
(root -> gen1 -> gen2), mirroring the multi-generation lineage from
the reported issue where a single conversation forked repeatedly."""
db.create_session(session_id="root", source="cli")
db.update_session_cwd("root", "/work/repo", git_repo_root="/work/repo")
db.create_session(session_id="gen1", source="cli", parent_session_id="root")
db.create_session(session_id="gen2", source="cli", parent_session_id="gen1")
assert db.get_session("gen1")["cwd"] == "/work/repo"
assert db.get_session("gen2")["cwd"] == "/work/repo"
def test_update_session_cwd_empty_branch_does_not_clobber(self, db):
"""A failed branch probe (empty string) must not wipe a branch we
already captured only the cwd updates."""