Files
kshitijk4poor 065ea31ad3 fix(cli): classify the /resume target with one session.resume
The switch asked the gateway twice about a refused target. The receipt-owner
lookup (_gateway_owned_id, 639bcdf4) attached first and swallowed every error;
_open_switched_session then attached again, because a refused id is never
cached, and only that second answer stopped the switch. The two calls had to
stay in that order, and the first one hid the refusal.

_gateway_open now makes the one attach and returns (owner_id, refusal):
refusal is the reason code when the owner answers RESUME_GONE (not_found,
permission_denied, profile_mismatch), else None (off the gateway store, a
transient refusal such as session_busy, or no answer). The switch uses it
directly; _gateway_owned_id keeps its signature as the owner half for the
startup compression-chain path. _open_switched_session goes away with its
stale docstring and the handler comment that described a pre-first-turn gap
_gateway_owned_id already closes on main.

The switch tests now run on a real GatewaySessionStore with a gateway double.
The previous ones used a MagicMock store, which is not a GatewaySessionStore,
so they skipped the gateway path and their "subscribes before the first turn"
case was red on main for that reason alone. The refused-target cases
(not_found, permission_denied, profile_mismatch) are red on main (the switch
lands on the target) and assert a single session.resume; the openable case
is a guard and passes on main.
2026-10-20 14:58:52 +05:30

44 lines
2.0 KiB
Python

"""Session-switch plumbing shared by /resume, /sessions and /branch.
The switch itself lives in ``cli_commands_mixin``; the handoff it needs sits here: end the row
being left, then repoint an already-built agent.
"""
from __future__ import annotations
from contextlib import suppress
def _end_current_session(cli, reason: str) -> None:
"""Flush un-persisted messages, then end the current session row with ``reason``.
Best-effort on both steps (the switch proceeds even if the DB write fails)."""
if cli.agent:
with suppress(Exception):
cli.agent._flush_messages_to_session_db(
cli.conversation_history, conversation_history=cli.conversation_history)
with suppress(Exception):
cli._session_db.end_session(cli.session_id, reason)
def _sync_agent_to_session(cli, session_id: str, *, parent_session_id: str, reason: str) -> None:
"""Point an already-built agent at ``session_id`` after a /resume or /branch switch: reset
per-session state, re-anchor the DB flush index, and notify memory providers with
reset=False (their state stays valid and just targets the new id; parent keeps lineage)."""
if not cli.agent:
return
cli.agent.session_id = session_id
cli.agent.reset_session_state()
if hasattr(cli.agent, "_last_flushed_db_idx"):
cli.agent._last_flushed_db_idx = len(cli.conversation_history)
if hasattr(cli.agent, "_todo_store"):
with suppress(Exception):
from tools.todo_tool import TodoStore
cli.agent._todo_store = TodoStore()
if hasattr(cli.agent, "_invalidate_system_prompt"):
cli.agent._invalidate_system_prompt()
with suppress(Exception):
_mm = getattr(cli.agent, "_memory_manager", None)
# reset=False: provider state follows the new id; parent_session_id keeps lineage (#6672).
if _mm is not None:
_mm.on_session_switch(
session_id, parent_session_id=parent_session_id or "", reset=False, reason=reason)