fix: better snapshot and gate

This commit is contained in:
Alex committed 2026-08-22 14:00:15 +01:00
1 parent 29f661f3c7
commit 3f88dc7b57
12 files changed
+449 -22

No files matched your search

+26 -3
View File
@@ -886,6 +886,31 @@ class ToolExecutor:
)
return True
@staticmethod
def _advertisable_action_names(tools_dict: Dict) -> set:
"""Action names a ``tools_dict`` would expose, mirroring pass 1 of
:meth:`prepare_tools_for_llm`.
The model calls ACTION names (``run_code``), never tool names
(``code_executor``), so a fallback built from ``tool["name"]`` names
strings that cannot resolve — feeding the very retry loop the
correctable error exists to break.
"""
names = set()
for tool in (tools_dict or {}).values():
tool = tool or {}
if tool.get("name") == "api_tool":
actions = (tool.get("config") or {}).get("actions") or {}
actions = actions.values() if isinstance(actions, dict) else actions
else:
actions = tool.get("actions") or []
for action in actions:
if not isinstance(action, dict) or not action.get("active", True):
continue
if action.get("name"):
names.add(str(action["name"]))
return names
def _available_tool_names(self, tools_dict: Dict, exclude: Optional[str] = None) -> str:
"""Render the names the model can actually call, for a correctable error.
@@ -909,9 +934,7 @@ class ToolExecutor:
if not scope or tool_id in scope
)
if not names:
names = sorted(
{(tool or {}).get("name") or tool_id for tool_id, tool in (tools_dict or {}).items()}
)
names = sorted(self._advertisable_action_names(tools_dict or {}))
names = [str(name) for name in names if name != exclude]
if len(names) > self.MAX_ADVERTISED_TOOL_NAMES:
hidden = len(names) - self.MAX_ADVERTISED_TOOL_NAMES
+11 -7
View File
@@ -27,13 +27,17 @@ config = context.config
if settings.POSTGRES_URI:
config.set_main_option("sqlalchemy.url", settings.POSTGRES_URI)
if config.config_file_name is not None:
# ``disable_existing_loggers`` defaults to True, which would switch off
# every ``application.*`` logger already imported by the time migrations
# run. ``app.py`` calls ``setup_logging()`` (line 16) BEFORE
# ``ensure_database_ready()`` (line 58), so the web tier never
# re-enables them and loses all application logging for the life of the
# process — on exactly the boot where a schema upgrade happened.
if config.config_file_name is not None and config.attributes.get("configure_logger", True):
# ``fileConfig`` reconfigures the ROOT logger from ``[logger_root]``
# (level WARNING, a stderr handler), which drops every ``application.*``
# INFO record, detaches the OTLP handler and the request/trace filter that
# ``setup_logging()`` installed, and cannot be undone by
# ``disable_existing_loggers=False`` — that flag only governs whether
# existing loggers are switched off. ``app.py`` calls ``setup_logging()``
# before ``ensure_database_ready()``, so an in-process migration would
# silence the web tier for the life of the process, on exactly the boot
# where a schema upgrade happened. In-process callers opt out via
# ``cfg.attributes``; the alembic CLI still gets its configured logging.
fileConfig(config.config_file_name, disable_existing_loggers=False)
+24 -2
View File
@@ -1437,6 +1437,25 @@ class LLMHandler(ABC):
tool_calls[call.index] = call
else:
existing = tool_calls[call.index]
# Decide BEFORE the id/name overwrite below: a gateway
# that reuses one index for sequential DISTINCT calls
# is indistinguishable from a restatement once the
# identity is gone. Restatement frames carry neither
# id nor name, so a differing one means a new call.
is_new_call = bool(
(call.id and existing.id and call.id != existing.id)
or (call.name and existing.name and call.name != existing.name)
)
if is_new_call:
logger.warning(
"tool_call_index_reused_by_distinct_call",
extra={
"index": call.index,
"previous_call": f"{existing.id}/{existing.name}",
"incoming_call": f"{call.id}/{call.name}",
"dropped_prefix": str(existing.arguments)[:120],
},
)
if call.id:
existing.id = call.id
if call.name:
@@ -1447,8 +1466,11 @@ class LLMHandler(ABC):
elif isinstance(existing.arguments, str) and isinstance(
call.arguments, str
):
if _is_restated_payload(
existing.arguments, call.arguments
if (
_is_restated_payload(
existing.arguments, call.arguments
)
and not is_new_call
):
# Some OpenAI-compatible gateways restate
# the WHOLE argument payload on the finish
+4
View File
@@ -400,6 +400,10 @@ def _run_migrations(log: logging.Logger) -> None:
cfg = Config(str(alembic_ini))
cfg.set_main_option("script_location", str(alembic_ini.parent / "alembic"))
# We migrate in-process, after setup_logging() has configured the root
# logger. env.py honours this by skipping fileConfig, which would
# otherwise replace root's handlers and level for the rest of the process.
cfg.attributes["configure_logger"] = False
# Cheap pre-check: if we're already at head, say so explicitly.
try:
@@ -176,7 +176,12 @@ class PendingToolStateRepository:
)
return result.rowcount > 0
def release_claim(self, conversation_id: str, user_id: str) -> bool:
def release_claim(
self,
conversation_id: str,
user_id: str,
ttl_extension_seconds: int = PENDING_STATE_TTL_SECONDS,
) -> bool:
"""Flip a ``resuming`` row back to ``pending`` so a retry can claim it.
The inverse of :meth:`mark_resuming`, for the process that TOOK the
@@ -187,8 +192,16 @@ class PendingToolStateRepository:
:meth:`load_state` only sees ``pending`` rows.
Deliberately narrow: it matches on ``status = 'resuming'`` only, so it
can never resurrect a row another request has since deleted, and it
does not touch ``expires_at`` (the TTL is unrelated to the claim).
can never resurrect a row another request has since deleted.
Extends ``expires_at`` like :meth:`revert_stale_resuming` does. A
resume claimed near the end of the TTL that fails minutes later would
otherwise be handed back already expired: ``load_state`` and
``mark_resuming`` both gate on ``expires_at > clock_timestamp()``, and
flipping to ``pending`` also hides the row from
:meth:`revert_stale_resuming`, which matches ``resuming`` only — so
releasing the claim would REMOVE the rescue instead of speeding it up.
``GREATEST`` so a failed resume can never shorten a healthy TTL.
Returns:
True when a claim was released.
@@ -197,13 +210,22 @@ class PendingToolStateRepository:
text(
"""
UPDATE pending_tool_state
SET status = 'pending', resumed_at = NULL
SET status = 'pending',
resumed_at = NULL,
expires_at = GREATEST(
expires_at,
clock_timestamp() + make_interval(secs => :ttl)
)
WHERE conversation_id = CAST(:conv_id AS uuid)
AND user_id = :user_id
AND status = 'resuming'
"""
),
{"conv_id": conversation_id, "user_id": user_id},
{
"conv_id": conversation_id,
"user_id": user_id,
"ttl": ttl_extension_seconds,
},
)
return result.rowcount > 0