fix: resolve Gemini CLI 0.37.0 session capture failures (#1664) (#1692)

Three root causes prevented Gemini sessions from persisting prompts,
observations, and summaries:

1. BeforeAgent was mapped to user-message (display-only) instead of
   session-init (which initialises the session and starts the SDK agent).

2. The transcript parser expected Claude Code JSONL (type: "assistant")
   but Gemini CLI 0.37.0 writes a JSON document with a messages array
   where assistant entries carry type: "gemini". extractLastMessage now
   detects the format and routes to the correct parser, preserving
   full backward compatibility with Claude Code JSONL transcripts.

3. The summarize handler omitted platformSource from the
   /api/sessions/summarize request body, causing sessions to be recorded
   without the gemini-cli source tag.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Ben Younes
2026-04-15 09:58:20 +02:00
committed by GitHub
parent 544e9d39f5
commit 983be42998
5 changed files with 321 additions and 4 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ import type { PlatformAdapter } from '../types.js';
* Notification → observation (system events like ToolPermission)
*
* Agent:
* BeforeAgent → user-message (captures user prompt)
* BeforeAgent → session-init (initializes session, captures user prompt)
* AfterAgent → observation (full agent response)
*
* Tool:
+5 -1
View File
@@ -18,6 +18,7 @@ import { ensureWorkerRunning, workerHttpRequest } from '../../shared/worker-util
import { logger } from '../../utils/logger.js';
import { extractLastMessage } from '../../shared/transcript-parser.js';
import { HOOK_EXIT_CODES, HOOK_TIMEOUTS, getTimeout } from '../../shared/hook-constants.js';
import { normalizePlatformSource } from '../../shared/platform-source.js';
const SUMMARIZE_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.DEFAULT);
const POLL_INTERVAL_MS = 500;
@@ -66,13 +67,16 @@ export const summarizeHandler: EventHandler = {
hasLastAssistantMessage: !!lastAssistantMessage
});
const platformSource = normalizePlatformSource(input.platform);
// 1. Queue summarize request — worker returns immediately with { status: 'queued' }
const response = await workerHttpRequest('/api/sessions/summarize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
last_assistant_message: lastAssistantMessage
last_assistant_message: lastAssistantMessage,
platformSource
}),
timeoutMs: SUMMARIZE_TIMEOUT_MS
});
@@ -80,7 +80,7 @@ const HOOK_TIMEOUT_MS = 10000;
*/
const GEMINI_EVENT_TO_INTERNAL_EVENT: Record<string, string> = {
'SessionStart': 'context',
'BeforeAgent': 'user-message',
'BeforeAgent': 'session-init',
'AfterAgent': 'observation',
'BeforeTool': 'observation',
'AfterTool': 'observation',
+77 -1
View File
@@ -3,7 +3,37 @@ import { logger } from '../utils/logger.js';
import { SYSTEM_REMINDER_REGEX } from '../utils/tag-stripping.js';
/**
* Extract last message of specified role from transcript JSONL file
* Detect whether a transcript file is in Gemini CLI JSON document format.
*
* Gemini CLI 0.37.0 writes a single JSON document with a top-level `messages`
* array instead of JSONL. Assistant entries use `type: "gemini"` rather than
* `type: "assistant"`.
*
* Example Gemini format:
* { "messages": [{ "type": "user", "content": "..." }, { "type": "gemini", "content": "..." }] }
*
* Claude Code format (JSONL):
* {"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
*/
function isGeminiTranscriptFormat(content: string): { isGemini: true; messages: any[] } | { isGemini: false } {
try {
const parsed = JSON.parse(content);
if (parsed && Array.isArray(parsed.messages)) {
return { isGemini: true, messages: parsed.messages };
}
} catch {
// Not a valid single JSON object — assume JSONL
}
return { isGemini: false };
}
/**
* Extract last message of specified role from transcript file.
*
* Supports two transcript formats:
* - JSONL (Claude Code): one JSON object per line, `type: "assistant"` or `type: "user"`
* - JSON document (Gemini CLI 0.37.0+): `{ messages: [{ type: "gemini"|"user", content: string }] }`
*
* @param transcriptPath Path to transcript file
* @param role 'user' or 'assistant'
* @param stripSystemReminders Whether to remove <system-reminder> tags (for assistant)
@@ -24,6 +54,52 @@ export function extractLastMessage(
return '';
}
// Gemini CLI 0.37.0 writes a JSON document rather than JSONL.
// Detect and handle it before falling through to the JSONL parser.
const geminiCheck = isGeminiTranscriptFormat(content);
if (geminiCheck.isGemini) {
return extractLastMessageFromGeminiTranscript(geminiCheck.messages, role, stripSystemReminders);
}
return extractLastMessageFromJsonl(content, role, stripSystemReminders);
}
/**
* Extract last message from Gemini CLI JSON document transcript.
* Maps `type: "gemini"` → assistant role; `type: "user"` → user role.
*/
function extractLastMessageFromGeminiTranscript(
messages: any[],
role: 'user' | 'assistant',
stripSystemReminders: boolean
): string {
// "gemini" entries are assistant turns; "user" entries are user turns
const geminiRole = role === 'assistant' ? 'gemini' : 'user';
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg?.type === geminiRole && typeof msg.content === 'string') {
let text = msg.content;
if (stripSystemReminders) {
text = text.replace(SYSTEM_REMINDER_REGEX, '');
text = text.replace(/\n{3,}/g, '\n\n').trim();
}
return text;
}
}
return '';
}
/**
* Extract last message from Claude Code JSONL transcript.
* Each line is an independent JSON object with `type: "assistant"` or `type: "user"`.
*/
function extractLastMessageFromJsonl(
content: string,
role: 'user' | 'assistant',
stripSystemReminders: boolean
): string {
const lines = content.split('\n');
let foundMatchingRole = false;