fix: thread injectable fetch through read tool and remote compaction
Completes the injectable-fetch transport wiring (15.10.8) that the feature left half-done, fixing the deterministic CI test failures: - compaction.compact() rebuilt summaryOptions field-by-field but dropped `fetch`, so the injected transport never reached requestOpenAiRemoteCompaction / generateSummary's remote path. Thread it. - Read-tool URL pipeline had no fetch seam: renderHtmlToText gained a fetchOverride param but renderUrl/ToolSession never carried one, so the jina/parallel reader backends always used global fetch. Add ToolSession.fetch -> renderUrl -> renderHtmlToText (defaults to global). - searchWithParallel mirrored extractWithParallel but missed the fetch option; add it. - Repair tests whose deleted hookFetch interceptors were never replaced with a FetchImpl seam (fetch-kagi-toggle, web-search-parallel, issue-970 discovery). - Update issue-1746 POSIX case to the #2154 preserved-scrollback contract: unknown-viewport streaming deferral is now platform-independent.
This commit is contained in:
@@ -996,6 +996,7 @@ export async function compact(
|
||||
// silently falls back to Effort.High — the same defect e07b47ee4 fixed
|
||||
// at the call sites, leaked back in here. See resolveCompactionEffort.
|
||||
thinkingLevel: options?.thinkingLevel,
|
||||
fetch: options?.fetch,
|
||||
};
|
||||
|
||||
let preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import type { FetchImpl, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
|
||||
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
||||
import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
|
||||
@@ -637,7 +637,7 @@ export async function renderHtmlToText(
|
||||
settings: Settings,
|
||||
userSignal: AbortSignal | undefined,
|
||||
storage: AgentStorage | null,
|
||||
fetchOverride?: typeof fetch,
|
||||
fetchOverride?: FetchImpl,
|
||||
): Promise<{ content: string; ok: boolean; method: string }> {
|
||||
const overallSignal = ptree.combineSignals(userSignal, timeout * 1000);
|
||||
const execOptions = {
|
||||
@@ -1060,6 +1060,7 @@ async function renderUrl(
|
||||
settings: Settings,
|
||||
signal: AbortSignal | undefined,
|
||||
storage: AgentStorage | null,
|
||||
fetchOverride?: FetchImpl,
|
||||
): Promise<FetchRenderResult> {
|
||||
const notes: string[] = [];
|
||||
const fetchedAt = new Date().toISOString();
|
||||
@@ -1433,7 +1434,15 @@ async function renderUrl(
|
||||
}
|
||||
|
||||
// 5E: Render HTML via the reader-backend chain (native/trafilatura/lynx/parallel/jina)
|
||||
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal, storage);
|
||||
const htmlResult = await renderHtmlToText(
|
||||
finalUrl,
|
||||
rawContent,
|
||||
timeout,
|
||||
settings,
|
||||
signal,
|
||||
storage,
|
||||
fetchOverride,
|
||||
);
|
||||
if (!htmlResult.ok) {
|
||||
notes.push("html rendering failed (no reader backend produced usable output)");
|
||||
|
||||
@@ -1634,7 +1643,7 @@ async function buildReadUrlCacheEntry(
|
||||
}
|
||||
|
||||
const storage = session.settings.getStorage();
|
||||
const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage);
|
||||
const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage, session.fetch);
|
||||
const output = buildUrlReadOutput(result, result.content);
|
||||
const artifactId = options?.ensureArtifact ? await persistReadUrlArtifact(session, output) : undefined;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
||||
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
||||
import type { FetchImpl, ToolChoice } from "@oh-my-pi/pi-ai";
|
||||
import { logger } from "@oh-my-pi/pi-utils";
|
||||
import type { AsyncJobManager } from "../async/job-manager";
|
||||
import type { PromptTemplate } from "../config/prompt-templates";
|
||||
@@ -142,6 +142,8 @@ export interface ToolSession {
|
||||
cwd: string;
|
||||
/** Whether UI is available */
|
||||
hasUI: boolean;
|
||||
/** Optional fetch implementation injected into the URL read pipeline (tests, proxies). Defaults to global fetch. */
|
||||
fetch?: FetchImpl;
|
||||
/** Skip Python kernel availability check and warmup */
|
||||
skipPythonPreflight?: boolean;
|
||||
/** Pre-loaded context files (AGENTS.md, etc) */
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface ParallelSearchOptions {
|
||||
mode?: "fast" | "research";
|
||||
maxCharsPerResult?: number;
|
||||
signal?: AbortSignal;
|
||||
fetch?: FetchImpl;
|
||||
}
|
||||
|
||||
export interface ParallelExtractOptions {
|
||||
@@ -296,7 +297,8 @@ export async function searchWithParallel(
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(PARALLEL_SEARCH_URL, {
|
||||
const fetchImpl = options.fetch ?? fetch;
|
||||
const response = await fetchImpl(PARALLEL_SEARCH_URL, {
|
||||
method: "POST",
|
||||
headers: getAuthHeaders(apiKey),
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -118,13 +118,7 @@ describe("issue #970 custom provider discovery", () => {
|
||||
});
|
||||
};
|
||||
|
||||
// NOTE: ModelRegistryImpl has no fetch injection seam; fetchMock cannot be
|
||||
// passed to the constructor. Missing API: ModelRegistry constructor (or
|
||||
// refreshProvider) must accept a `fetch` option to avoid global override.
|
||||
// Tracked: packages/coding-agent/src/config/model-registry.ts #discoverOpenAIModelsList
|
||||
void fetchMock;
|
||||
|
||||
const registry = new ModelRegistryImpl(authStorage, modelsPath);
|
||||
const registry = new ModelRegistryImpl(authStorage, modelsPath, { fetch: fetchMock });
|
||||
await registry.refreshProvider("vllm");
|
||||
|
||||
const providerModels = registry.getAll().filter(model => model.provider === "vllm");
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as scrapers from "@oh-my-pi/pi-coding-agent/web/scrapers/types";
|
||||
import * as scraperUtils from "@oh-my-pi/pi-coding-agent/web/scrapers/utils";
|
||||
import * as natives from "@oh-my-pi/pi-natives";
|
||||
import { ptree, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { asGlobalFetch } from "../helpers/fetch-mock";
|
||||
|
||||
const withMissingSystemPython = () => {
|
||||
const whichSpy = vi.spyOn(Bun, "which").mockImplementation(() => null);
|
||||
@@ -494,6 +495,7 @@ describe("read tool URL handling", () => {
|
||||
|
||||
it("uses section-scoped llms.txt fallback without requesting the site-wide file", async () => {
|
||||
const session = createSession();
|
||||
session.fetch = asGlobalFetch(() => new Response("blocked", { status: 500, statusText: "Blocked" }));
|
||||
const tool = new ReadTool(session);
|
||||
const pageUrl = "https://example.com/docs/reference/widget";
|
||||
const pageHtml = "<html><body><nav>Docs</nav><main><h1>Widget</h1></main></body></html>";
|
||||
@@ -577,6 +579,32 @@ describe("read tool URL handling", () => {
|
||||
const session = createSession({ "providers.fetch": "parallel" });
|
||||
const tool = new ReadTool(session);
|
||||
const pageUrl = "https://example.com/parallel-page";
|
||||
session.fetch = asGlobalFetch(input => {
|
||||
if (String(input) === "https://api.parallel.ai/v1beta/extract") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
extract_id: "extract-fetch-1",
|
||||
results: [
|
||||
{
|
||||
url: pageUrl,
|
||||
title: "Parallel Page",
|
||||
excerpts: [
|
||||
"Parallel-rendered content that is comfortably longer than one hundred characters. ".repeat(
|
||||
2,
|
||||
),
|
||||
],
|
||||
full_content: null,
|
||||
},
|
||||
],
|
||||
errors: [],
|
||||
warnings: null,
|
||||
usage: null,
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
return new Response("blocked", { status: 500, statusText: "Blocked" });
|
||||
});
|
||||
const pageHtml = "<html><body><main><h1>Parallel Page</h1></main></body></html>";
|
||||
const ensureToolSpy = vi.spyOn(toolsManager, "ensureTool");
|
||||
const htmlToMarkdownSpy = vi.spyOn(natives, "htmlToMarkdown");
|
||||
|
||||
@@ -78,10 +78,7 @@ describe("Parallel web search", () => {
|
||||
usage: [{ name: "sku_search", count: 1 }],
|
||||
});
|
||||
|
||||
// NOTE: searchWithParallel (web/parallel.ts) has no fetch seam; global fetch still used here.
|
||||
void fetchMock;
|
||||
|
||||
const result = await searchWithParallel("parallel query", ["parallel query"], {}, fakeStorage);
|
||||
const result = await searchWithParallel("parallel query", ["parallel query"], { fetch: fetchMock }, fakeStorage);
|
||||
expect(capturedRequestBody).toEqual({
|
||||
objective: "parallel query",
|
||||
search_queries: ["parallel query"],
|
||||
|
||||
@@ -211,12 +211,14 @@ describe("issue #1746: scrolled reader in a non-WT ConPTY host (Tabby)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps POSIX eager streaming rebuilds destructive (win32 guard must not leak)", async () => {
|
||||
// Control case: on POSIX (non-ED3-risk terminal), the same eager
|
||||
// streaming mutation IS allowed to rebuild history live — that is the
|
||||
// documented purpose of setEagerNativeScrollbackRebuild. The win32
|
||||
// deferral must stay scoped to Windows, or streaming tool output would
|
||||
// stop reaching native scrollback on POSIX until the next checkpoint.
|
||||
it("defers unknown-viewport streaming rebuilds on POSIX too (deferral is platform-independent)", async () => {
|
||||
// Companion to the win32 case: an unknown viewport probe is not proof the
|
||||
// reader is at the tail on ANY platform, so #2154 routes every eager
|
||||
// streaming mutation over an unknown viewport through incremental
|
||||
// append/repaint primitives instead of the destructive historyRebuild
|
||||
// (ED3). The deferral is not a win32-only guard — POSIX with an unknown
|
||||
// viewport defers identically, keeping offscreen growth in scrollback
|
||||
// instead of clearing it once per streaming tick. See #2154.
|
||||
await withEnvPatch(CONPTY_HOST_ENV, async () => {
|
||||
await withPlatform("linux", async () => {
|
||||
await withTerminalRisk(false, async () => {
|
||||
@@ -240,7 +242,7 @@ describe("issue #1746: scrolled reader in a non-WT ConPTY host (Tabby)", () => {
|
||||
tui.requestRender();
|
||||
await settle(term);
|
||||
|
||||
expect(eraseScrollbackCount(writes)).toBe(1);
|
||||
expect(eraseScrollbackCount(writes)).toBe(0);
|
||||
} finally {
|
||||
tui.stop();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user