fix: key the tools[] pin by code version; never re-add config-excluded tools

Review follow-up on the byte-identical tools[] pin.

- The pin records the code identity that built it (checkout/build sha, else
  the release version). Written by the same code, every pinned tool that is
  still available keeps its pinned bytes, including tools whose parameters
  are derived per surface (delegate_task, text_to_speech, memory, patch).
  The per-tool "parameters differ -> take current" rule replaced those bytes
  on every surface hop and rewrote the ~44KB pin each time. A pin from other
  code (`hermes update`, legacy name lists) takes the current definitions
  once and is re-pinned.
- A pinned tool this process did not build is carried forward only while
  this agent's toolset selection allows it (enabled minus disabled toolsets
  and role reservations, before check_fn). It must also pass the session
  schema gates on the merged array, so browser_exec never comes back once
  terminal is gone. Client-surface toolsets (desktop_ui, project) still
  carry across hops: no config choice removed them there.
- The rotation compaction child inherits the parent's pin in the publish
  transaction.
- `hermes sessions recover` keeps pin rows in its system_prompts sweep and
  clears dangling pin hashes, as lost-and-found now does too. Profile moves
  carry the pin like the prompt. A continuing session whose pin is missing
  or unreadable (a row swept by an older build) pins the tools it sends on
  that turn, so later hops stay stable.
This commit is contained in:
teknium1
2026-09-23 09:23:01 -07:00
committed by Teknium
parent da70c46124
commit d956f0ae57
12 changed files with 211 additions and 63 deletions

View File

@@ -661,12 +661,12 @@ class SessionSessionsMixin:
self._delete_unreferenced_system_prompts(conn)
self._execute_write(_do)
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 update_session_tool_names(self, session_id: str, pin: Any) -> None:
"""Persist the session's ``tools[]`` pin (JSON-serializable) 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(pin) if pin 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))