fix(state): inherit git_branch + gateway origin columns on compression children

Follow-ups on top of #64731's cwd/git_repo_root inheritance:

- git_branch joins the parent-row backfill (same NULL-only COALESCE hop):
  the Desktop sidebar branch chip otherwise vanishes at every compaction
  boundary even though the workspace didn't change.
- Belt-and-suspenders for #59527: compression forks (parent already ended
  with end_reason='compression') also inherit the gateway origin columns
  (user_id/session_key/chat_id/chat_type/thread_id/display_name/
  origin_json) at DB-level child creation. The gateway re-records the peer
  after rotation (d5b4879d4), but a hard crash in the window between child
  creation and that write left the child unrecoverable by
  find_latest_gateway_session_for_peer. Scoped to compression forks only —
  delegate/subagent children (parent still live) must NOT inherit routing
  keys, or peer recovery could repoint gateway traffic into a subagent's
  session.
- Behavioral test driving the real _compress_context rotation path,
  asserting the child row carries cwd/git_repo_root/git_branch and the
  origin columns.
This commit is contained in:
Teknium
2026-07-22 05:54:54 -07:00
parent 3c74c12554
commit 75099ca0ef
3 changed files with 169 additions and 9 deletions
+59 -9
View File
@@ -2730,14 +2730,19 @@ class SessionDB:
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.
spawn, branch continuation) and this row's own ``cwd``/``git_repo_root``/
``git_branch`` 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. For compression forks specifically
(parent ended with ``end_reason='compression'``), the gateway origin
columns (``user_id``/``session_key``/``chat_id``/``chat_type``/
``thread_id``/``display_name``/``origin_json``) are inherited too, so a
crash before the gateway re-records the peer can't strand the child
without a recoverable routing mapping (#59527).
"""
def _do(conn):
conn.execute(
@@ -2785,10 +2790,55 @@ class SessionDB:
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 p.id = sessions.parent_session_id)),
git_branch = COALESCE(sessions.git_branch,
(SELECT p.git_branch FROM sessions p
WHERE p.id = sessions.parent_session_id))
WHERE id = ? AND parent_session_id IS NOT NULL""",
(session_id,),
)
# Belt-and-suspenders for gateway routing metadata (#59527):
# the gateway re-records the peer on the child after rotation
# (d5b4879d4), but a hard crash between child creation and that
# write leaves the child row without origin columns, so
# ``find_latest_gateway_session_for_peer`` can't recover the
# mapping on restart. Inherit them from the parent at creation
# time — but ONLY for compression forks (parent already ended
# with end_reason='compression'). Delegate/subagent children
# are spawned while the parent is still live and must NOT
# inherit routing keys, or peer recovery could repoint gateway
# traffic into a subagent's session.
conn.execute(
"""UPDATE sessions
SET user_id = COALESCE(sessions.user_id,
(SELECT p.user_id FROM sessions p
WHERE p.id = sessions.parent_session_id)),
session_key = COALESCE(sessions.session_key,
(SELECT p.session_key FROM sessions p
WHERE p.id = sessions.parent_session_id)),
chat_id = COALESCE(sessions.chat_id,
(SELECT p.chat_id FROM sessions p
WHERE p.id = sessions.parent_session_id)),
chat_type = COALESCE(sessions.chat_type,
(SELECT p.chat_type FROM sessions p
WHERE p.id = sessions.parent_session_id)),
thread_id = COALESCE(sessions.thread_id,
(SELECT p.thread_id FROM sessions p
WHERE p.id = sessions.parent_session_id)),
display_name = COALESCE(sessions.display_name,
(SELECT p.display_name FROM sessions p
WHERE p.id = sessions.parent_session_id)),
origin_json = COALESCE(sessions.origin_json,
(SELECT p.origin_json FROM sessions p
WHERE p.id = sessions.parent_session_id))
WHERE id = ? AND parent_session_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM sessions p
WHERE p.id = sessions.parent_session_id
AND p.end_reason = 'compression'
)""",
(session_id,),
)
self._execute_write(_do)
def create_session(self, session_id: str, source: str, **kwargs) -> str: