Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2660,11 +2660,11 @@ async def start(self) -> bool:
pass
else:
try:
suspended = self.session_store.suspend_recently_active()
if suspended:
logger.info("Suspended %d in-flight session(s) from previous run", suspended)
resumed = self.session_store.suspend_recently_active()
if resumed:
logger.info("Marked %d in-flight session(s) as resume-pending from previous run", resumed)
except Exception as e:
logger.warning("Session suspension on startup failed: %s", e)
logger.warning("Session resume-pending marking failed: %s", e)

# Stuck-loop detection (#7536): if a session has been active across
# 3+ consecutive restarts, it's probably stuck in a loop (the same
Expand Down Expand Up @@ -6367,7 +6367,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g

# Auto voice reply: send TTS audio before the text response
_already_sent = bool(agent_result.get("already_sent"))
if self._should_send_voice_reply(event, response, agent_messages, already_sent=_already_sent):
if self._should_send_voice_reply(event, response, agent_messages, already_sent=_already_sent, history_offset=agent_result.get("history_offset", len(agent_messages))):
await self._send_voice_reply(event, response)

# If streaming already delivered the response, extract and
Expand Down Expand Up @@ -8096,6 +8096,7 @@ def _should_send_voice_reply(
response: str,
agent_messages: list,
already_sent: bool = False,
history_offset: int = 0,
) -> bool:
"""Decide whether the runner should send a TTS voice reply.

Expand All @@ -8122,14 +8123,15 @@ def _should_send_voice_reply(
if not should:
return False

# Dedup: agent already called TTS tool
# Dedup: agent already called TTS tool (current turn only, not full history)
current_turn_messages = agent_messages[history_offset:] if history_offset else agent_messages
has_agent_tts = any(
msg.get("role") == "assistant"
and any(
tc.get("function", {}).get("name") == "text_to_speech"
for tc in (msg.get("tool_calls") or [])
)
for msg in agent_messages
for msg in current_turn_messages
)
if has_agent_tts:
return False
Expand Down
34 changes: 18 additions & 16 deletions gateway/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,19 +1086,20 @@ def prune_old_entries(self, max_age_days: int) -> int:
return len(removed_keys)

def suspend_recently_active(self, max_age_seconds: int = 120) -> int:
"""Mark recently-active sessions as suspended.

Called on gateway startup to prevent sessions that were likely
in-flight when the gateway last exited from being blindly resumed
(#7536). Only suspends sessions updated within *max_age_seconds*
to avoid resetting long-idle sessions that are harmless to resume.
Returns the number of sessions that were suspended.

Entries flagged ``resume_pending=True`` are skipped — those were
marked intentionally by the drain-timeout path as recoverable.
Terminal escalation for genuinely stuck ``resume_pending`` sessions
is handled by the existing ``.restart_failure_counts`` stuck-loop
counter, which runs after this method on startup.
"""Mark recently-active sessions as resume-pending after dirty shutdown.

Called on gateway startup when the previous exit was not clean
(no .clean_shutdown marker). Instead of hard-suspending sessions
(which wipes conversation history), marks them as resume_pending
so the user can seamlessly continue where they left off.

Stuck-loop escalation (3+ consecutive dirty restarts) is handled
separately by ``_suspend_stuck_loop_sessions()`` which runs after
this method — genuinely broken sessions still get suspended.

Entries flagged ``resume_pending=True`` or ``suspended=True`` are
skipped to avoid overriding explicit user actions (/stop).
Returns the number of sessions that were marked.
"""
from datetime import timedelta

Expand All @@ -1107,10 +1108,11 @@ def suspend_recently_active(self, max_age_seconds: int = 120) -> int:
with self._lock:
self._ensure_loaded_locked()
for entry in self._entries.values():
if entry.resume_pending:
if entry.resume_pending or entry.suspended:
continue
if not entry.suspended and entry.updated_at >= cutoff:
entry.suspended = True
if entry.updated_at >= cutoff:
entry.resume_pending = True
entry.resume_reason = "dirty_shutdown"
count += 1
if count:
self._save()
Expand Down