Merge 3ddf79eba7 into da71eaebd2
This commit is contained in:
commit
a965190306
@ -5,6 +5,76 @@ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||
import type { EffectiveContextPruningSettings } from "./settings.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;
|
||||
// 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.
|
||||
@ -228,6 +298,12 @@ export function pruneContextMessages(params: {
|
||||
if (hasImageBlocks(msg.content)) {
|
||||
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);
|
||||
|
||||
const updated = softTrimToolResultMessage({
|
||||
|
||||
@ -21,6 +21,35 @@ function resolveTelegramApiLogger(runtime?: RuntimeEnv, logger?: TelegramApiLogg
|
||||
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>({
|
||||
operation,
|
||||
fn,
|
||||
@ -32,9 +61,9 @@ export async function withTelegramApiErrorLogging<T>({
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
if (!shouldLog || shouldLog(err)) {
|
||||
const errText = formatErrorMessage(err);
|
||||
const message = buildErrorMessage(operation, err);
|
||||
const log = resolveTelegramApiLogger(runtime, logger);
|
||||
log(danger(`telegram ${operation} failed: ${errText}`));
|
||||
log(danger(message));
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
@ -7,8 +7,9 @@ import { resolveTelegramAutoSelectFamilyDecision } from "./network-config.js";
|
||||
let appliedAutoSelectFamily: boolean | null = null;
|
||||
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
|
||||
// This issue appears to be resolved in Node 24+.
|
||||
function applyTelegramNetworkWorkarounds(network?: TelegramNetworkConfig): void {
|
||||
const decision = resolveTelegramAutoSelectFamilyDecision({ network });
|
||||
if (decision.value === null || decision.value === appliedAutoSelectFamily) return;
|
||||
|
||||
@ -32,8 +32,13 @@ export function resolveTelegramAutoSelectFamilyDecision(params?: {
|
||||
if (typeof params?.network?.autoSelectFamily === "boolean") {
|
||||
return { value: params.network.autoSelectFamily, source: "config" };
|
||||
}
|
||||
if (Number.isFinite(nodeMajor) && nodeMajor >= 22) {
|
||||
return { value: false, source: "default-node22" };
|
||||
// Node 22-23 workaround: disable autoSelectFamily to avoid Happy Eyeballs timeouts.
|
||||
// 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 };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user