From 054b62621eac3cee9bb59bfb7be25c09d0e0ac87 Mon Sep 17 00:00:00 2001 From: Tomasz Mazur <47872060+AHGIJMKLKKZNPJKQR@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:38:50 +0100 Subject: [PATCH 1/4] Ensure game stops after timeout --- deep-sea-stories/packages/backend/src/agent/gemini/session.ts | 3 ++- deep-sea-stories/packages/backend/src/game/room.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/deep-sea-stories/packages/backend/src/agent/gemini/session.ts b/deep-sea-stories/packages/backend/src/agent/gemini/session.ts index de1aaa0..be8f94e 100644 --- a/deep-sea-stories/packages/backend/src/agent/gemini/session.ts +++ b/deep-sea-stories/packages/backend/src/agent/gemini/session.ts @@ -57,7 +57,7 @@ export class GeminiSession implements VoiceAgentSession { this.session.sendClientContent({ turns: [ { - text: 'IMPORTANT: The game time has expired. You must now: 1) Tell the players that time is up, 2) Evaluate how close they were to solving the riddle, 3) IMMEDIATELY call the endGame function to close the game session. Do not wait for player response - call endGame right after your message.', + text: 'IMPORTANT: The game time has expired. You must now: 1) Tell the players that time is up, 2) Evaluate how close they were to solving the riddle.', }, ], turnComplete: true, @@ -73,6 +73,7 @@ export class GeminiSession implements VoiceAgentSession { async open() { if (this.opening) return; this.opening = true; + this.ending = false; const params: LiveConnectParameters = { model: GEMINI_MODEL, diff --git a/deep-sea-stories/packages/backend/src/game/room.ts b/deep-sea-stories/packages/backend/src/game/room.ts index 30e260d..1245fac 100644 --- a/deep-sea-stories/packages/backend/src/game/room.ts +++ b/deep-sea-stories/packages/backend/src/game/room.ts @@ -187,6 +187,7 @@ export class GameRoom { console.log(`⏰ Game time limit reached for room ${this.roomId}`); try { await this.gameSession?.announceTimeExpired(); + await this.stopGame(true) } catch (e) { console.error('Error announcing time expired:', e); } From cf91ff3028a6a1730cce9c2f2d17cf3594d9d30f Mon Sep 17 00:00:00 2001 From: Tomasz Mazur <47872060+AHGIJMKLKKZNPJKQR@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:41:18 +0100 Subject: [PATCH 2/4] Format --- deep-sea-stories/packages/backend/src/game/room.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deep-sea-stories/packages/backend/src/game/room.ts b/deep-sea-stories/packages/backend/src/game/room.ts index 1245fac..a0b1be7 100644 --- a/deep-sea-stories/packages/backend/src/game/room.ts +++ b/deep-sea-stories/packages/backend/src/game/room.ts @@ -187,7 +187,7 @@ export class GameRoom { console.log(`⏰ Game time limit reached for room ${this.roomId}`); try { await this.gameSession?.announceTimeExpired(); - await this.stopGame(true) + await this.stopGame(true); } catch (e) { console.error('Error announcing time expired:', e); } From 4f26eadb993111fc98fb81d8fe4ec93342a923e8 Mon Sep 17 00:00:00 2001 From: Tomasz Mazur <47872060+AHGIJMKLKKZNPJKQR@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:39:24 +0100 Subject: [PATCH 3/4] Make stopGame idempotent --- .../backend/src/agent/gemini/session.ts | 16 ++++++++++++- .../packages/backend/src/game/room.ts | 24 +++++++++---------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/deep-sea-stories/packages/backend/src/agent/gemini/session.ts b/deep-sea-stories/packages/backend/src/agent/gemini/session.ts index be8f94e..4528861 100644 --- a/deep-sea-stories/packages/backend/src/agent/gemini/session.ts +++ b/deep-sea-stories/packages/backend/src/agent/gemini/session.ts @@ -10,9 +10,12 @@ import { getInstructionsForStory } from '../../utils.js'; import type { AgentConfig } from '../api.js'; import type { VoiceAgentSession } from '../session.js'; +const AGENT_EXPIRATION_MAX_SECONDS = 15; + export class GeminiSession implements VoiceAgentSession { private onInterrupt: (() => void) | null = null; private onAgentAudio: ((audio: Buffer) => void) | null = null; + private onTurnEnd: (() => void) | null = null; private session: Session | null = null; private transcriptionParts: string[] = []; private genai: GoogleGenAI; @@ -57,11 +60,20 @@ export class GeminiSession implements VoiceAgentSession { this.session.sendClientContent({ turns: [ { - text: 'IMPORTANT: The game time has expired. You must now: 1) Tell the players that time is up, 2) Evaluate how close they were to solving the riddle.', + text: 'IMPORTANT: The game time has expired. You must now: 1) Tell the players that time is up, 2) Evaluate how close they were to solving the riddle, 3) Call the `endGame` function tool.', }, ], turnComplete: true, }); + + return new Promise((resolve) => { + const timeout = setTimeout(resolve, AGENT_EXPIRATION_MAX_SECONDS * 1000); + this.onTurnEnd = () => { + this.onTurnEnd = null; + clearTimeout(timeout); + resolve(); + }; + }); } async waitUntilDone() { @@ -200,6 +212,8 @@ export class GeminiSession implements VoiceAgentSession { this.transcriptionParts = []; } + if (turnFinished) this.onTurnEnd?.(); + const base64 = message.data; if (base64) { this.handleAgentAudio(Buffer.from(base64, 'base64')); diff --git a/deep-sea-stories/packages/backend/src/game/room.ts b/deep-sea-stories/packages/backend/src/game/room.ts index a0b1be7..6f11be5 100644 --- a/deep-sea-stories/packages/backend/src/game/room.ts +++ b/deep-sea-stories/packages/backend/src/game/room.ts @@ -195,6 +195,11 @@ export class GameRoom { } async stopGame(wait: boolean = false) { + if (!this.gameSession) return; + + const gameSession = this.gameSession; + this.gameSession = null; + console.log('Stopping game room %s', this.roomId); if (this.gameTimeoutId) { clearTimeout(this.gameTimeoutId); @@ -202,25 +207,20 @@ export class GameRoom { } try { - if (this.gameSession) { - await this.gameSession.stopGame(wait); - await this.fishjamClient.deletePeer( - this.roomId, - this.gameSession.agentId, - ); - } - } catch (e) { - if (!(e instanceof PeerNotFoundException)) throw e; - } finally { - this.gameSession = null; - this.story = undefined; + await gameSession.stopGame(wait); this.notifierService.emitNotification(this.roomId, { type: 'gameEnded' as const, timestamp: Date.now(), }); + await this.fishjamClient.deletePeer(this.roomId, gameSession.agentId); + } catch (e) { + if (!(e instanceof PeerNotFoundException)) throw e; + } finally { + this.story = undefined; this.gameStarted = false; + console.log(`Stopped game for room ${this.roomId}`); } } From 9b5945478ed9bcb47821b87ce721216b052e6f38 Mon Sep 17 00:00:00 2001 From: Tomasz Mazur <47872060+AHGIJMKLKKZNPJKQR@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:03:26 +0100 Subject: [PATCH 4/4] Revert prompt change --- deep-sea-stories/packages/backend/src/agent/gemini/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deep-sea-stories/packages/backend/src/agent/gemini/session.ts b/deep-sea-stories/packages/backend/src/agent/gemini/session.ts index 4528861..9be0679 100644 --- a/deep-sea-stories/packages/backend/src/agent/gemini/session.ts +++ b/deep-sea-stories/packages/backend/src/agent/gemini/session.ts @@ -60,7 +60,7 @@ export class GeminiSession implements VoiceAgentSession { this.session.sendClientContent({ turns: [ { - text: 'IMPORTANT: The game time has expired. You must now: 1) Tell the players that time is up, 2) Evaluate how close they were to solving the riddle, 3) Call the `endGame` function tool.', + text: 'IMPORTANT: The game time has expired. You must now: 1) Tell the players that time is up, 2) Evaluate how close they were to solving the riddle, 3) IMMEDIATELY call the endGame function to close the game session. Do not wait for player response - call endGame right after your message.', }, ], turnComplete: true,