Skip to content
Draft
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
96 changes: 4 additions & 92 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@chat-adapter/state-memory": "^4.20.0",
"@chat-adapter/telegram": "^4.20.0",
"@clack/prompts": "^1.0.1",
"@google/gemini-cli": "0.34.0-nightly.20260307.6c3a90645",
"@google/gemini-cli": "0.37.1",
"@mariozechner/pi-tui": "^0.54.2",
"@modelcontextprotocol/sdk": "^1",
"@tobilu/qmd": "^1.1.6",
Expand Down Expand Up @@ -79,6 +79,6 @@
],
"patchedDependencies": {
"@tobilu/qmd@1.1.6": "patches/@tobilu%2Fqmd@1.1.6.patch",
"@google/gemini-cli@0.34.0-nightly.20260307.6c3a90645": "patches/@google%2Fgemini-cli@0.34.0-nightly.20260307.6c3a90645.patch"
"@google/gemini-cli@0.37.1": "patches/@google%2Fgemini-cli@0.37.1.patch"
}
}

This file was deleted.

77 changes: 77 additions & 0 deletions patches/@google%2Fgemini-cli@0.37.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
diff --git a/bundle/gemini.js b/bundle/gemini.js
index 3b159ccc8c2b9cb75409fc0adf059ccf5c661118..ca78782a7e8ce84efdd6ea322292937171f4e05f 100755
--- a/bundle/gemini.js
+++ b/bundle/gemini.js
@@ -12532,7 +12532,7 @@ var GeminiAgent = class _GeminiAgent {
this.settings
);
this.sessions.set(sessionId2, session);
- session.streamHistory(sessionData.messages);
+ // [geminiclaw patch] streamHistory() removed — prevents late notification contamination
setTimeout(() => {
session.sendAvailableCommands();
}, 0);
@@ -12803,6 +12803,8 @@ ${thought.description}`;
let totalInputTokens = 0;
let totalOutputTokens = 0;
const modelUsageMap = /* @__PURE__ */ new Map();
+ // [geminiclaw patch] capture full usageMetadata (incl. thoughts/cached) + modelVersion
+ let _lastUsageMetadata, _lastModelVersion;
let nextMessage = { role: "user", parts };
while (nextMessage !== null) {
if (pendingSend.signal.aborted) {
@@ -12823,7 +12825,7 @@ ${thought.description}`;
const router = this.context.config.getModelRouterService();
const { model } = await router.route(routingContext);
const responseStream = await chat.sendMessageStream(
- { model },
+ { model, isChatModel: true },
nextMessage?.parts ?? [],
promptId,
pendingSend.signal,
@@ -12843,6 +12845,11 @@ ${thought.description}`;
if (resp.value.modelVersion) {
turnModelId = resp.value.modelVersion;
}
+ // [geminiclaw patch] retain full usageMetadata + modelVersion for _meta
+ _lastUsageMetadata = resp.value.usageMetadata;
+ if (resp.value.modelVersion) {
+ _lastModelVersion = resp.value.modelVersion;
+ }
}
if (resp.type === StreamEventType.CHUNK && resp.value.candidates && resp.value.candidates.length > 0) {
const candidate = resp.value.candidates[0];
@@ -12906,7 +12913,10 @@ ${thought.description}`;
}
})
)
- }
+ },
+ // [geminiclaw patch] preserve full usageMetadata + modelVersion
+ usageMetadata: _lastUsageMetadata,
+ modelVersion: _lastModelVersion
}
};
}
@@ -12942,7 +12952,10 @@ ${thought.description}`;
output_tokens: totalOutputTokens
},
model_usage: modelUsageArray
- }
+ },
+ // [geminiclaw patch] preserve full usageMetadata + modelVersion
+ usageMetadata: _lastUsageMetadata,
+ modelVersion: _lastModelVersion
}
};
}
@@ -14503,7 +14516,8 @@ async function main() {
process.exit(ExitCodes.FATAL_AUTHENTICATION_ERROR);
}
let stdinData = "";
- if (!process.stdin.isTTY) {
+ // [geminiclaw patch] ACP bypass — readStdin would consume JSON-RPC frames
+ if (!process.stdin.isTTY && !argv.acp && !argv.experimentalAcp) {
stdinData = await readStdin();
}
const injectStdinIntoArgs = (args, stdinData2) => {
58 changes: 30 additions & 28 deletions src/agent/acp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,9 @@

import { type ChildProcess, spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import {
closeSync,
existsSync,
fstatSync,
openSync,
readFileSync,
readSync,
realpathSync,
writeFileSync,
} from 'node:fs';
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
import { platform } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { dirname, resolve } from 'node:path';
import { createInterface } from 'node:readline';
import { loadConfig } from '../../config/io.js';
import { getGeminiBin } from '../../config/paths.js';
Expand All @@ -38,6 +29,7 @@ import type {
AcpNewSessionParams,
AcpNewSessionResult,
AcpPromptParams,
AcpPromptPart,
AcpSessionUpdate,
JsonRpcError,
JsonRpcResponse,
Expand Down Expand Up @@ -470,12 +462,16 @@ export class AcpClient {
*
* Returns when the prompt response arrives (i.e. the model is done).
*/
async prompt(sessionId: string, text: string, timeoutMs?: number): Promise<JsonRpcResponse> {
const params: AcpPromptParams = {
sessionId,
prompt: [{ type: 'text', text }],
};
const ms = timeoutMs ?? DEFAULT_TIMEOUT_MS;
async prompt(
sessionId: string,
text: string,
options?: { parts?: AcpPromptPart[]; timeoutMs?: number },
): Promise<JsonRpcResponse> {
const promptParts: AcpPromptPart[] = options?.parts
? [{ type: 'text', text }, ...options.parts]
: [{ type: 'text', text }];
const params: AcpPromptParams = { sessionId, prompt: promptParts };
const ms = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
this.activePromptId = this.nextId; // peek at the ID that send() will use
try {
return await this.send('session/prompt', params, ms);
Expand All @@ -484,6 +480,19 @@ export class AcpClient {
}
}

/**
* Switch the model on an existing session without respawning.
*
* Uses the `unstable_setSessionModel` ACP method (v0.35+).
* Falls back gracefully if the method is unavailable.
*/
async setSessionModel(sessionId: string, modelId: string): Promise<void> {
const resp = await this.send('unstable_setSessionModel', { sessionId, modelId }, 30_000);
if (resp.error) {
throw new Error(`setSessionModel failed: ${resp.error.message}`);
}
}

/**
* Reset the timeout timer for the active prompt request.
* Call this when tool activity is detected to prevent killing
Expand Down Expand Up @@ -640,17 +649,10 @@ function buildSeatbeltEnv(_cwd: string, geminiArgs: string[], env: Record<string
*/
function assertSandboxPatchApplied(): void {
try {
const bundlePath = join(dirname(realpathSync(getGeminiBin())), 'gemini.js');
// The readStdin patch is near the end of the 25MB bundle, so read
// the last 200KB instead of the first 600KB.
const fd = openSync(bundlePath, 'r');
const stat = fstatSync(fd);
const tailSize = Math.min(200_000, stat.size);
const buf = Buffer.alloc(tailSize);
readSync(fd, buf, 0, tailSize, stat.size - tailSize);
closeSync(fd);
const tail = buf.toString('utf-8');
if (tail.includes('isTTY && !argv.acp') || tail.includes('isTTY && !argv.experimentalAcp')) return; // patched
// .bin/gemini symlinks directly to bundle/gemini.js in v0.37+
const targetPath = realpathSync(getGeminiBin());
const content = readFileSync(targetPath, 'utf-8');
if (content.includes('isTTY && !argv.acp') || content.includes('isTTY && !argv.experimentalAcp')) return; // patched
} catch {
return; // can't verify — proceed optimistically
}
Expand Down
34 changes: 32 additions & 2 deletions src/agent/acp/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import type { AcpClient, SandboxMode } from './client.js';
import { AcpEventMapper, extractModelVersion, extractUsageMetadata, synthesizeResultEvent } from './event-mapper.js';
import { AcpProcessPool } from './process-pool.js';
import type { AcpMcpServerEntry } from './types.js';
import type { AcpMcpServerEntry, AcpPromptPart } from './types.js';

const log = createLogger('acp-runner');

Expand All @@ -42,6 +42,20 @@ export function abortSession(targetLaneSessionId: string): boolean {
return true;
}

/**
* Switch the model on the first active session.
*
* Uses `unstable_setSessionModel` — falls back gracefully if unavailable.
* Returns the laneSessionId that was switched, or undefined if no active session.
*/
export async function switchActiveModel(modelId: string): Promise<string | undefined> {
for (const [laneSessionId, { client, sessionId }] of activeRuns) {
await client.setSessionModel(sessionId, modelId);
return laneSessionId;
}
return undefined;
}

export interface SpawnGeminiAcpOptions extends SpawnGeminiOptions {
/** MCP server configurations to inject via session/new. */
mcpServers?: AcpMcpServerEntry[];
Expand All @@ -55,6 +69,8 @@ export interface SpawnGeminiAcpOptions extends SpawnGeminiOptions {
poolPriority?: 'normal' | 'background';
/** Sandbox mode: true (auto-detect), false (disabled), 'seatbelt', or 'docker'. */
sandbox?: SandboxMode;
/** Multimodal prompt parts (images, audio) to send alongside text. */
multimodalParts?: AcpPromptPart[];
}

/**
Expand Down Expand Up @@ -138,6 +154,17 @@ export async function spawnGeminiAcp(options: SpawnGeminiAcpOptions): Promise<Ru
const newResult = await client.newSession(options.cwd, options.mcpServers);
sessionId = newResult.sessionId;
resolvedModelId = newResult.models?.currentModelId;
if (newResult.models?.availableModels?.length) {
log.info('available models', {
models: newResult.models.availableModels.map((m) => m.modelId).join(', '),
});
}
if (newResult.modes?.availableModes?.length) {
log.info('available modes', {
modes: newResult.modes.availableModes.map((m) => m.id).join(', '),
current: newResult.modes.currentModeId,
});
}
}
log.info('ACP session ready', { ms: Date.now() - sessionMs });

Expand Down Expand Up @@ -314,7 +341,10 @@ export async function spawnGeminiAcp(options: SpawnGeminiAcpOptions): Promise<Ru

let resp: Awaited<ReturnType<typeof client.prompt>>;
try {
resp = await client.prompt(sessionId, promptText, options.timeoutMs);
resp = await client.prompt(sessionId, promptText, {
parts: options.multimodalParts,
timeoutMs: options.timeoutMs,
});
} finally {
clearInterval(askUserPoller);
}
Expand Down
29 changes: 27 additions & 2 deletions src/agent/acp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,28 @@ export interface AcpNewSessionParams {
mcpServers?: AcpMcpServerEntry[];
}

export interface AcpAvailableModel {
modelId: string;
name: string;
description?: string;
}

export interface AcpAvailableMode {
id: string;
name: string;
description?: string;
}

export interface AcpNewSessionResult {
sessionId: string;
models?: { currentModelId?: string };
models?: {
currentModelId?: string;
availableModels?: AcpAvailableModel[];
};
modes?: {
currentModeId?: string;
availableModes?: AcpAvailableMode[];
};
}

export interface AcpLoadSessionParams {
Expand All @@ -125,9 +144,15 @@ export interface AcpLoadSessionParams {
mcpServers?: AcpMcpServerEntry[];
}

/** A single part of an ACP prompt (text, image, or audio). */
export type AcpPromptPart =
| { type: 'text'; text: string }
| { type: 'image'; mimeType: string; data: string }
| { type: 'audio'; mimeType: string; data: string };

export interface AcpPromptParams {
sessionId: string;
prompt: Array<{ type: 'text'; text: string }>;
prompt: AcpPromptPart[];
}

export interface AcpPromptResult {
Expand Down
2 changes: 1 addition & 1 deletion src/agent/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export class RunResultBuilder {
this.result.toolCalls.push(toolCall);

// Detect skill activation from activate_skill tool calls
if (event.tool_name === 'activate_skill') {
if (event.tool_name === 'activate_skill' || event.tool_name?.endsWith('_activate_skill')) {
const skillName = extractSkillNameFromToolArgs(event.parameters);
if (skillName) this.result.skillActivations.push(skillName);
}
Expand Down
Loading
Loading