mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
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:
parent
b6a2b701c1
commit
3c74c12554
2 changed files with 70 additions and 3 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue