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 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({
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user