This commit is contained in:
priyajohnvenky 2026-01-30 11:55:31 +00:00 committed by GitHub
commit a965190306
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 116 additions and 5 deletions

View File

@ -5,6 +5,76 @@ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
import type { EffectiveContextPruningSettings } from "./settings.js"; import type { EffectiveContextPruningSettings } from "./settings.js";
import { makeToolPrunablePredicate } from "./tools.js"; import { makeToolPrunablePredicate } from "./tools.js";
interface ToolCallLike {
id: string;
name?: string;
}
function extractToolCallsFromAssistant(
msg: Extract<AgentMessage, { role: "assistant" }>,
): ToolCallLike[] {
const content = msg.content;
if (!Array.isArray(content)) return [];
const toolCalls: ToolCallLike[] = [];
for (const block of content) {
if (!block || typeof block !== "object") continue;
const rec = block as { type?: unknown; id?: unknown; name?: unknown };
if (typeof rec.id !== "string" || !rec.id) continue;
if (rec.type === "tool_use" || rec.type === "toolCall") {
toolCalls.push({
id: rec.id,
name: typeof rec.name === "string" ? rec.name : undefined,
});
}
}
return toolCalls;
}
function extractToolResultId(msg: Extract<AgentMessage, { role: "toolResult" }>): string | null {
const toolCallId = (msg as { toolCallId?: unknown }).toolCallId;
if (typeof toolCallId === "string" && toolCallId) return toolCallId;
const toolUseId = (msg as { toolUseId?: unknown }).toolUseId;
if (typeof toolUseId === "string" && toolUseId) return toolUseId;
return null;
}
/**
* Validates that a tool_result at the given index has a corresponding tool_use
* in the previous assistant message. This prevents orphaned tool_result blocks
* that would cause API validation errors.
*/
function hasCorrespondingToolUse(messages: AgentMessage[], toolResultIndex: number): boolean {
const toolResult = messages[toolResultIndex];
if (!toolResult || toolResult.role !== "toolResult") return false;
const toolResultId = extractToolResultId(
toolResult as Extract<AgentMessage, { role: "toolResult" }>,
);
if (!toolResultId) return false;
// Scan backwards to find the assistant message with the matching tool_use
for (let j = toolResultIndex - 1; j >= 0; j--) {
const prevMsg = messages[j];
if (!prevMsg) continue;
// Stop at the first assistant message (tool_results must follow their assistant message)
if (prevMsg.role === "assistant") {
const toolCalls = extractToolCallsFromAssistant(
prevMsg as Extract<AgentMessage, { role: "assistant" }>,
);
return toolCalls.some((tc) => tc.id === toolResultId);
}
// Stop if we hit a user message (shouldn't happen in valid history)
if (prevMsg.role === "user") {
return false;
}
}
return false;
}
const CHARS_PER_TOKEN_ESTIMATE = 4; const CHARS_PER_TOKEN_ESTIMATE = 4;
// We currently skip pruning tool results that contain images. Still, we count them (approx.) so // We currently skip pruning tool results that contain images. Still, we count them (approx.) so
// we start trimming prunable tool results earlier when image-heavy context is consuming the window. // we start trimming prunable tool results earlier when image-heavy context is consuming the window.
@ -228,6 +298,12 @@ export function pruneContextMessages(params: {
if (hasImageBlocks(msg.content)) { if (hasImageBlocks(msg.content)) {
continue; continue;
} }
// CRITICAL: Validate that this tool_result has a corresponding tool_use
// in the previous assistant message. Pruning orphaned tool_results causes
// API validation errors: "unexpected tool_use_id found in tool_result blocks"
if (!hasCorrespondingToolUse(messages, i)) {
continue;
}
prunableToolIndexes.push(i); prunableToolIndexes.push(i);
const updated = softTrimToolResultMessage({ const updated = softTrimToolResultMessage({

View File

@ -21,6 +21,35 @@ function resolveTelegramApiLogger(runtime?: RuntimeEnv, logger?: TelegramApiLogg
return (message: string) => fallbackLogger.error(message); return (message: string) => fallbackLogger.error(message);
} }
function isNetworkError(err: unknown): boolean {
const errStr = String(err).toLowerCase();
return (
errStr.includes("network request") ||
errStr.includes("fetch failed") ||
errStr.includes("econnrefused") ||
errStr.includes("enotfound") ||
errStr.includes("etimedout") ||
errStr.includes("econnreset")
);
}
function buildErrorMessage(operation: string, err: unknown): string {
const errText = formatErrorMessage(err);
let message = `telegram ${operation} failed: ${errText}`;
// Add helpful hints for network errors
if (isNetworkError(err)) {
message +=
"\nTroubleshooting network errors:\n" +
" 1. Check internet connectivity to api.telegram.org\n" +
" 2. If using Node 22-23, try: clawdbot config set channels.telegram.network.autoSelectFamily true\n" +
" 3. Check for proxy/firewall blocking Telegram API\n" +
" 4. Verify no local DNS resolution issues";
}
return message;
}
export async function withTelegramApiErrorLogging<T>({ export async function withTelegramApiErrorLogging<T>({
operation, operation,
fn, fn,
@ -32,9 +61,9 @@ export async function withTelegramApiErrorLogging<T>({
return await fn(); return await fn();
} catch (err) { } catch (err) {
if (!shouldLog || shouldLog(err)) { if (!shouldLog || shouldLog(err)) {
const errText = formatErrorMessage(err); const message = buildErrorMessage(operation, err);
const log = resolveTelegramApiLogger(runtime, logger); const log = resolveTelegramApiLogger(runtime, logger);
log(danger(`telegram ${operation} failed: ${errText}`)); log(danger(message));
} }
throw err; throw err;
} }

View File

@ -7,8 +7,9 @@ import { resolveTelegramAutoSelectFamilyDecision } from "./network-config.js";
let appliedAutoSelectFamily: boolean | null = null; let appliedAutoSelectFamily: boolean | null = null;
const log = createSubsystemLogger("telegram/network"); const log = createSubsystemLogger("telegram/network");
// Node 22 workaround: disable autoSelectFamily to avoid Happy Eyeballs timeouts. // Node 22-23 workaround: disable autoSelectFamily to avoid Happy Eyeballs timeouts.
// See: https://github.com/nodejs/node/issues/54359 // See: https://github.com/nodejs/node/issues/54359
// This issue appears to be resolved in Node 24+.
function applyTelegramNetworkWorkarounds(network?: TelegramNetworkConfig): void { function applyTelegramNetworkWorkarounds(network?: TelegramNetworkConfig): void {
const decision = resolveTelegramAutoSelectFamilyDecision({ network }); const decision = resolveTelegramAutoSelectFamilyDecision({ network });
if (decision.value === null || decision.value === appliedAutoSelectFamily) return; if (decision.value === null || decision.value === appliedAutoSelectFamily) return;

View File

@ -32,8 +32,13 @@ export function resolveTelegramAutoSelectFamilyDecision(params?: {
if (typeof params?.network?.autoSelectFamily === "boolean") { if (typeof params?.network?.autoSelectFamily === "boolean") {
return { value: params.network.autoSelectFamily, source: "config" }; return { value: params.network.autoSelectFamily, source: "config" };
} }
if (Number.isFinite(nodeMajor) && nodeMajor >= 22) { // Node 22-23 workaround: disable autoSelectFamily to avoid Happy Eyeballs timeouts.
return { value: false, source: "default-node22" }; // See: https://github.com/nodejs/node/issues/54359
// This issue appears to be resolved in Node 24+, where autoSelectFamily works correctly.
if (Number.isFinite(nodeMajor) && nodeMajor >= 22 && nodeMajor < 24) {
return { value: false, source: "default-node22-23" };
} }
// Node 24+: Use Node's default behavior (autoSelectFamily enabled by default)
// Node 25 testing shows autoSelectFamily=true works correctly for Telegram API
return { value: null }; return { value: null };
} }