fix: one session sends byte-identical tools[] across TUI, oneshot and gateway hops
The session tools pin (sessions.tool_names) stored names only, so every fresh
process re-materialized the bytes from its own surface and every surface hop
of one durable session was a full prompt-cache miss:
* tool_search's deferred catalog is built per process ("Search 6 additional
tools" in the TUI gateway vs 5 in -q);
* a pinned tool missing from the fresh build (skill_manage under the -q
footprint) came back from the static registry schema, without its
dynamic_schema_overrides;
* a -q --resume that rebuilt the stored prompt (model switch, cwd drift)
persisted its own pruned array over the pin.
The pin now stores the full definitions and restore replays a pinned tool that
is still available byte-for-byte (deregistered tools drop, new ones append at
the tail, legacy name-only pins still work). A continuing session whose prompt
is rebuilt applies the pin before building it, matching the freeze policy
(tools[] only changes on /new, /reload-mcp, compaction). The array is
content-addressed in the existing system_prompts store like the prompt itself,
so identical arrays across sessions are stored once; get_session resolves it.
This commit is contained in:
@@ -661,11 +661,17 @@ class SessionSessionsMixin:
|
||||
self._delete_unreferenced_system_prompts(conn)
|
||||
self._execute_write(_do)
|
||||
|
||||
def update_session_tool_names(self, session_id: str, tool_names: Optional[List[str]]) -> None:
|
||||
"""Persist the resolved ``tools[]`` name order so a rebuilt AIAgent can't fork the cached tool
|
||||
prefix on a flipped check_fn verdict; ``None`` clears."""
|
||||
payload = json.dumps(list(tool_names)) if tool_names is not None else None
|
||||
self._write_sql("UPDATE sessions SET tool_names = ? WHERE id = ?", (payload, session_id))
|
||||
def update_session_tool_names(self, session_id: str, tools: Optional[List[Any]]) -> None:
|
||||
"""Persist the session's ``tools[]`` pin so a rebuilt AIAgent sends the same bytes; ``None``
|
||||
clears. The array repeats across sessions like a system prompt does, so it is stored in the
|
||||
same content-addressed ``system_prompts`` table and the column holds its hash (legacy rows:
|
||||
an inline JSON name list); ``get_session`` resolves either."""
|
||||
payload = json.dumps(list(tools)) if tools is not None else None
|
||||
def _do(conn):
|
||||
conn.execute("UPDATE sessions SET tool_names = ? WHERE id = ?",
|
||||
(self._store_system_prompt(conn, payload), session_id))
|
||||
self._delete_unreferenced_system_prompts(conn)
|
||||
self._execute_write(_do)
|
||||
|
||||
def update_session_model(
|
||||
self, session_id: str, model: str, provider: Optional[str] = None, *,
|
||||
@@ -781,8 +787,10 @@ class SessionSessionsMixin:
|
||||
"""Get a session by ID (drains queued token deltas first so cost readers see exact totals)."""
|
||||
self.flush_token_counts()
|
||||
row = self._read_one(
|
||||
"SELECT s.*, COALESCE(sp.prompt, s.system_prompt) AS _system_prompt_resolved "
|
||||
"FROM sessions s LEFT JOIN system_prompts sp ON sp.hash = s.system_prompt_hash WHERE s.id = ?",
|
||||
"SELECT s.*, COALESCE(sp.prompt, s.system_prompt) AS _system_prompt_resolved, "
|
||||
"COALESCE(tp.prompt, s.tool_names) AS _tool_names_resolved "
|
||||
"FROM sessions s LEFT JOIN system_prompts sp ON sp.hash = s.system_prompt_hash "
|
||||
"LEFT JOIN system_prompts tp ON tp.hash = s.tool_names WHERE s.id = ?",
|
||||
(session_id,),
|
||||
)
|
||||
return self._session_row_dict(row) if row else None
|
||||
|
||||
Reference in New Issue
Block a user